Test an Orleans application end to end
This walkthrough builds Orleans runtime tests in layers: a first in-process cluster test, a shared cluster fixture, and a topology change. The complete, buildable source is in the documentation’s grains/snippets/testing/orleans-testing project.
Choose the right boundary
Section titled “Choose the right boundary”Use ordinary unit tests for pure application logic, InProcessTestCluster for Orleans runtime behavior, and production-provider tests for external contracts.
These boundaries preserve fast feedback and runtime fidelity.
Run the first cluster test
Section titled “Run the first cluster test”Clone the repository, then run the maintained test project:
git clone https://github.com/dotnet/orleans.gitcd orleansdotnet test .\docs\site\src\content\docs\grains\snippets\testing\orleans-testing\Sample.OrleansTesting\Sample.OrleansTesting.csprojThe first test creates a cluster, deploys it, obtains a grain reference from the client, makes a call, and disposes the cluster:
public sealed class HelloGrainTests : IAsyncLifetime{ private InProcessTestCluster _cluster = null!;
public async Task 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 Task DisposeAsync() => await _cluster.DisposeAsync();}This validates code generation, serialization, activation, message dispatch, and the grain implementation together. The default builder starts two silos and a client with fast in-memory test infrastructure.
Add application configuration
Section titled “Add application configuration”Production grains commonly depend on services registered through dependency injection. Configure all hosts, only silos, or only the client using the matching builder scope:
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 owns a service provider. Registering a service type creates one instance per host. Registering a captured instance intentionally shares it across silos and the client, so shared test doubles must be thread-safe.
Run the tests again after each configuration change. Propagate deployment failures through test setup so the test run reports them as failures.
Reuse an expensive cluster
Section titled “Reuse an expensive cluster”Create an xUnit fixture when multiple tests need identical cluster configuration:
public sealed class ClusterFixture : IAsyncLifetime{ public InProcessTestCluster Cluster { get; private set; } = null!;
public async Task InitializeAsync() { var builder = new InProcessTestClusterBuilder(); Cluster = builder.Build(); await Cluster.DeployAsync(); }
public async Task DisposeAsync() => await Cluster.DisposeAsync();}Register the fixture as a collection:
[CollectionDefinition(Name)]public sealed class ClusterCollection : ICollectionFixture<ClusterFixture>{ public const string Name = nameof(ClusterCollection);}Then consume the fixture’s already-started cluster from every test:
[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); }}Give each test unique grain identities, reset shared external state, and make every test order-independent.
Exercise a topology change
Section titled “Exercise a topology change”Add a silo, wait for membership to stabilize, then stop it gracefully:
public static async Task AddAndRemoveSiloAsync( InProcessTestCluster cluster){ var addedSilo = await cluster.StartAdditionalSiloAsync(); await cluster.WaitForLivenessToStabilizeAsync();
await cluster.StopSiloAsync(addedSilo); await cluster.WaitForLivenessToStabilizeAsync();}Invoke the helper from a test and verify an application call after the membership changes:
public sealed class TopologyTests : IAsyncLifetime{ private InProcessTestCluster _cluster = null!;
public async Task InitializeAsync() { _cluster = ClusterConfiguration.Create(new SharedTestState()); await _cluster.DeployAsync(); }
[Fact] public async Task GrainCallSucceedsAfterSiloJoinsAndLeaves() { await ClusterConfiguration.AddAndRemoveSiloAsync(_cluster);
var hello = _cluster.Client.GetGrain<IHelloGrain>(Guid.NewGuid()); var greeting = await hello.SayHello("World");
Assert.Equal("Hello, World!", greeting); }
public async Task DisposeAsync() => await _cluster.DisposeAsync();}The documented dotnet test command executes this test. A same-process cluster exercises membership changes. Use a separate environment to exercise process crashes, network partitions, socket transport, and production membership providers.
Add production-provider tests
Section titled “Add production-provider tests”For persistence, reminders, clustering, or streams, add an opt-in suite which:
- Provisions an isolated provider instance or emulator.
- Configures the cluster with the same provider extension used in production.
- Uses unique database, table, stream, and grain identifiers.
- Verifies behavior across a silo restart.
- Cleans up owned resources even when an assertion fails.
Store credentials in the test environment’s secret facility and report missing prerequisites explicitly. For API details and fidelity boundaries, see Test Orleans applications and TestingHost architecture.
