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
InProcessTestClusterMost grain tests, including grain calls, activation, scheduling, serialization, dependency injection, and cluster behavior
OrleansTestKitBasic arrange-act-assert tests of one grain activation whose correctness is independent of Orleans scheduling and concurrency
Test cluster with production providersStorage, 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.

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 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.

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. 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);
}
}

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.

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.

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:

Terminal window
dotnet add package OrleansTestKit

The 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.