Skip to content

Test Orleans applications

Orleans applications benefit from tests at more than one level:

Test levelBest suited for
Isolated unit testPure application logic and constructor-injected collaborators
InProcessTestClusterGrain calls, activation, scheduling, serialization, dependency injection, and cluster behavior
Test cluster with production providersStorage, clustering, reminders, and streams whose external-system contract matters

Use the smallest level which preserves the behavior under test. A mock can verify that code called a collaborator, but it cannot reproduce Orleans turn scheduling or message serialization. Conversely, starting a cluster for a pure calculation adds cost without increasing confidence.

Install the Microsoft.Orleans.TestingHost package in the test project:

Terminal window
dotnet add package Microsoft.Orleans.TestingHost

For 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 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();
}

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.

The builder exposes separate configuration scopes:

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.

Cluster startup is more expensive than an isolated unit test. Reuse a cluster across tests which need the same configuration by using an xUnit collection fixture:

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();
}
[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);
}
}

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.

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.

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.

Test pure domain behavior without a cluster when the code does not depend on the Orleans runtime. Prefer constructor injection or extract the behavior into an ordinary service which is easy to instantiate. A mocked IGrainFactory or grain reference can verify collaboration, but it does not validate:

  • grain activation or deactivation;
  • request interleaving and reentrancy;
  • serialization and deep copying;
  • placement, directory lookup, or message routing; or
  • provider and lifecycle behavior.

Do not widen production APIs solely so a mocking framework can override inherited runtime members. When any runtime behavior above is part of the assertion, use a test cluster instead.