Test Orleans applications
Orleans applications benefit from tests at more than one level:
| Test level | Best suited for |
|---|---|
| Isolated unit test | Pure application logic and constructor-injected collaborators |
| InProcessTestCluster | Most grain tests, including grain calls, activation, scheduling, serialization, dependency injection, and cluster behavior |
| OrleansTestKit | Basic arrange-act-assert tests of one grain activation whose correctness is independent of Orleans scheduling and concurrency |
| Test cluster with production providers | Storage, clustering, reminders, and streams whose external-system contract matters |
Default to InProcessTestCluster for grain code. It provides the highest-fidelity test boundary for Orleans runtime behavior. Test extracted calculations and application services directly. Use OrleansTestKit when the test author controls sequencing and synchronization.
Create an in-process test cluster
Section titled “Create an in-process test cluster”Install the Microsoft.Orleans.TestingHost package in the test project:
dotnet add package Microsoft.Orleans.TestingHostFor new Orleans tests, prefer InProcessTestClusterBuilder. Build the cluster, call DeployAsync, use its client, and dispose the cluster after the test:
public sealed class HelloGrainTests : IAsyncLifetime{ private InProcessTestCluster _cluster = null!;
public async ValueTask InitializeAsync() { var builder = new InProcessTestClusterBuilder(); _cluster = builder.Build(); await _cluster.DeployAsync(); }
[Fact] public async Task SaysHelloCorrectly() { var hello = _cluster.Client.GetGrain<IHelloGrain>(Guid.NewGuid()); var greeting = await hello.SayHello("World");
Assert.Equal("Hello, World!", greeting); }
public async ValueTask DisposeAsync() => await _cluster.DisposeAsync();}The default builder starts two silos and a client using in-memory transport, test membership, and a test grain directory. These substitutions make tests fast, but they do not reproduce socket transport or a production membership provider. See TestingHost architecture for the complete fidelity boundaries.
Configure hosts, silos, and clients
Section titled “Configure hosts, silos, and clients”The builder exposes separate configuration scopes:
- ConfigureHost applies to every silo host and the client host.
- ConfigureSilo applies to every silo.
- ConfigureClient applies to the cluster client.
public static InProcessTestCluster Create(SharedTestState sharedState){ var builder = new InProcessTestClusterBuilder(initialSilosCount: 2);
builder.ConfigureHost(hostBuilder => { hostBuilder.Services.AddSingleton(sharedState); });
builder.ConfigureSilo((siloOptions, siloBuilder) => { siloBuilder.AddMemoryGrainStorageAsDefault(); siloBuilder.Services.AddSingleton(siloOptions); });
builder.ConfigureClient(clientBuilder => { clientBuilder.Services.AddSingleton<ClientTestService>(); });
return builder.Build();}Each host has its own dependency-injection container. Registering a type creates one singleton per container. Registering a captured instance, as in the example, deliberately shares that instance with the client and all silos. Shared mutable test doubles must therefore be thread-safe.
Reuse a cluster across tests
Section titled “Reuse a cluster across tests”Cluster startup is more expensive than an isolated unit test. Test framework lifecycle support lets tests with the same configuration reuse a cluster.
Create an xUnit collection fixture:
public sealed class ClusterFixture : IAsyncLifetime{ public InProcessTestCluster Cluster { get; private set; } = null!;
public async ValueTask InitializeAsync() { var builder = new InProcessTestClusterBuilder(); Cluster = builder.Build(); await Cluster.DeployAsync(); }
public async ValueTask DisposeAsync() => await Cluster.DisposeAsync();}Register the fixture as a collection and apply that collection to each test class that shares the cluster:
[CollectionDefinition(Name)]public sealed class ClusterCollection : ICollectionFixture<ClusterFixture>{ public const string Name = nameof(ClusterCollection);}[Collection(ClusterCollection.Name)]public sealed class HelloGrainTestsWithFixture(ClusterFixture fixture){ [Fact] public async Task SaysHelloCorrectly() { var hello = fixture.Cluster.Client.GetGrain<IHelloGrain>(Guid.NewGuid()); var greeting = await hello.SayHello("World");
Assert.Equal("Hello, World!", greeting); }}MSTest
Section titled “MSTest”Group MSTest classes that share cluster configuration in a dedicated test project. Use assembly lifecycle methods to deploy one cluster for that project:
[TestClass]public sealed class MSTestClusterFixture{ public static InProcessTestCluster Cluster { get; private set; } = null!;
[AssemblyInitialize] public static async Task Initialize(Microsoft.VisualStudio.TestTools.UnitTesting.TestContext _) { var builder = new InProcessTestClusterBuilder(); Cluster = builder.Build(); await Cluster.DeployAsync(); }
[AssemblyCleanup] public static async Task Cleanup() => await Cluster.DisposeAsync();}Tests access the deployed cluster through the fixture:
[TestClass]public sealed class HelloGrainMSTests{ [TestMethod] public async Task SharedClusterSaysHelloCorrectly() { var hello = MSTestClusterFixture.Cluster.Client.GetGrain<IHelloGrain>(Guid.NewGuid()); var greeting = await hello.SayHello("World");
MSTestAssert.AreEqual("Hello, World!", greeting); }}Shared-cluster tests must not depend on execution order. Give each test distinct grain identities and reset any shared external state. Use separate fixtures when suites require incompatible silo or provider configuration.
Change cluster topology
Section titled “Change cluster topology”An in-process cluster can add and remove silos while a test is running:
public static async Task AddAndRemoveSiloAsync( InProcessTestCluster cluster){ var addedSilo = await cluster.StartAdditionalSiloAsync(); await cluster.WaitForLivenessToStabilizeAsync();
await cluster.StopSiloAsync(addedSilo); await cluster.WaitForLivenessToStabilizeAsync();}Wait for liveness to stabilize before asserting behavior which depends on the new membership view. StopSiloAsync performs a graceful stop. Abrupt process loss, network partitioning, and production transport behavior require a harness which introduces those failure modes explicitly.
Maintain existing TestCluster suites
Section titled “Maintain existing TestCluster suites”TestCluster remains available for suites built around class-based configurators such as ISiloConfigurator. Its built-in hosts run in process; a custom silo-creation delegate is required for another isolation model. New Orleans tests should generally use InProcessTestCluster for its delegate-based configuration and direct access to each host’s service provider.
Choose the runtime boundary
Section titled “Choose the runtime boundary”Use InProcessTestCluster when an assertion depends on runtime behavior. The cluster creates activations through the Orleans runtime and preserves turn scheduling, interleaving, serialization, dependency injection, persistence integration, placement, message routing, timers, reminders, and lifecycle behavior.
Pure domain logic can be extracted into an ordinary class or service and tested directly. Keep that boundary independent of Orleans runtime abstractions. A cluster test can replace application-owned collaborators through dependency injection while the runtime continues to create and execute the grain.
Configure the production storage or reminder provider when the test covers its external-system contract, concurrency behavior, or restart recovery.
Use OrleansTestKit for a basic single-activation test
Section titled “Use OrleansTestKit for a basic single-activation test”OrleansTestKit is a community project in the OrleansContrib organization. It creates a fixture for one grain activation and supplies test implementations for activation identity, persistent state, grain references, timers, reminders, and streams. The test invokes grain code on its own execution context, so the test author controls sequencing and synchronization. This boundary suits basic arrange-act-assert tests of a single method whose result depends on injected values and recorded collaborator interactions. Match the OrleansTestKit major version to the Orleans major version used by the application.
Install the package in the test project:
dotnet add package OrleansTestKitThe following grain uses its string identity to address another grain and persists an item before making that call:
public interface IShoppingCartGrain : IGrainWithStringKey{ Task AddItem(string item);}
public interface IAuditGrain : IGrainWithStringKey{ Task RecordItemAdded(string item);}
[GenerateSerializer]public sealed class ShoppingCartState{ [Id(0)] public List<string> Items { get; set; } = [];}
public sealed class ShoppingCartGrain( [PersistentState("cart")] IPersistentState<ShoppingCartState> state) : Grain, IShoppingCartGrain{ public async Task AddItem(string item) { state.State.Items.Add(item); await state.WriteStateAsync();
var audit = GrainFactory.GetGrain<IAuditGrain>(this.GetPrimaryKeyString()); await audit.RecordItemAdded(item); }}Derive the test class from TestKitBase. Register persistent state and grain probes on Silo before creating the grain with its test identity:
public sealed class ShoppingCartGrainTests : TestKitBase{ [Fact] public async Task AddItemPersistsStateAndNotifiesAuditGrain() { var state = new ShoppingCartState(); Silo.AddPersistentState("cart", state: state); var audit = Silo.AddProbe<IAuditGrain>("customer-42"); var grain = await Silo.CreateGrainAsync<ShoppingCartGrain>("customer-42");
await grain.AddItem("coffee");
Assert.Equal(["coffee"], state.Items); Assert.Equal(1, Silo.StorageManager.GetStorageStats("cart")?.Writes); audit.Verify(grain => grain.RecordItemAdded("coffee"), Times.Once); }}The test verifies the grain’s state mutation, storage write request, key-derived grain reference, and outgoing call. OrleansTestKit invokes one grain using its simulated activation context and resolves collaborating grains as probes.
Use InProcessTestCluster for grains that await work, make concurrent calls, use reentrancy or interleaving, coordinate multiple activations, or depend on serialization, lifecycle, placement, timers, reminders, streams, or message routing. The runtime owns those behaviors, and the in-process cluster preserves their execution model.
The OrleansTestKit README documents version compatibility and package setup. Its test suite demonstrates the fixture APIs and available test doubles for single-activation tests.
