Since lesson 4.5.2 you have been writing tests against a cluster inside the test process. Every lesson has brought a piece of craft along: a storage that survives a restart, a fake clock, a board that counts signals. This lesson collects the craft, adds what is still missing, and walks you through the mistake that most often makes tests with Orleans unreliable: a test that is always green on your machine and sometimes red in the pipeline.
The InProcessTestCluster
InProcessTestCluster from Microsoft.Orleans.TestingHost starts silos and a client inside the test process. Messages between them run over
a transport in memory, not over sockets. The silos do have addresses with ports, but those are only names; no test occupies a
port of the machine for them. Several test clusters can therefore run at the same time, even in parallel test projects.
It is set up through the builder:
| Call | affects |
|---|---|
new InProcessTestClusterBuilder(initialSilosCount: 2) |
the number of silos at the start (one by default); the parameter is a short |
ConfigureSilo((options, silo) => …) |
every silo, including those a test starts later |
ConfigureClient(client => …) |
the test cluster's client |
ConfigureHost(host => …) |
silos and client together |
Shared services
Because everything runs in one process, the test and all silos can use the same object. You have used that since lesson 4.5.3: the
StoredRows object holds the stored rows outside the silos, and a restart or a second silo finds the same data, just as
several silos in production find the same database. In the same way, the test and the silos share the fake clock from lesson 4.5.7 and the in-memory event store from
lesson 4.5.5.
Share only what is also shared in production: database, clock, external services. A cache that in production every silo has for itself does not belong in a shared object in the test. Otherwise one silo in the test sees what another has just written, and an error that in production occurs only with two silos stays invisible in the test.
One configuration for all grain tests
So far every test class has set up its cluster itself, each a little differently. The Kladde's host has only one configuration, though.
A small shared test project tests/Kladde.Testing therefore holds the building blocks that every grain test project needs:
// tests/Kladde.Testing/KladdeTestCluster.cs
using Kladde.GrainStorage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Time.Testing;
using Orleans.Reminders;
using Orleans.Storage;
using Orleans.TestingHost;
using Xunit;
namespace Kladde.Testing;
// One configuration for every grain test of the Kladde: the same storage format, reminders and clocks as the host.
public static class KladdeTestCluster
{
public static InProcessTestCluster Build(FakeTimeProvider clock, StoredRows rows, short silos, Action<ISiloBuilder>? configure = null)
{
var builder = new InProcessTestClusterBuilder(initialSilosCount: silos);
builder.ConfigureSilo((_, silo) =>
{
silo.UseInMemoryReminderService();
silo.Services.AddSingleton<TimeProvider>(clock);
silo.Services.UseTimeProviderForBackgroundAreas(TimeProvider.System);
silo.Services.AddKeyedSingleton<TimeProvider>(ReminderTimeProviderNames.Reminders, clock);
silo.Services.AddKeyedSingleton<IGrainStorage>("quotes", (_, _) => new SharedGrainStorage(rows, new KladdeStateSerializer()));
configure?.Invoke(silo);
});
return builder.Build();
}
}
public class KladdeClusterFixture : IAsyncLifetime
{
public FakeTimeProvider Clock { get; } = new(new DateTimeOffset(2025, 3, 20, 9, 0, 0, TimeSpan.Zero));
public StoredRows Rows { get; } = new();
public InProcessTestCluster Cluster { get; private set; } = null!;
protected virtual short Silos => 1;
public async ValueTask InitializeAsync()
{
Cluster = KladdeTestCluster.Build(Clock, Rows, Silos, Configure);
await Cluster.DeployAsync();
}
public async ValueTask DisposeAsync()
{
await Cluster.DisposeAsync();
GC.SuppressFinalize(this);
}
protected virtual void Configure(ISiloBuilder silo)
{
}
}
public sealed class TwoSiloFixture : KladdeClusterFixture
{
protected override short Silos => 2;
}
StoredRows and SharedGrainStorage from lesson 4.5.3 move into the same project, along with TestBoard and GcStorm from this lesson. A
test project with needs of its own derives: sales adds the CustomStorage provider and its in-memory event store in Configure, and nothing
more. That way the test cluster differs from the host only where that is intended: storage in memory instead of SQL
Server, a fake clock instead of the real one.
Three rules keep tests that share a cluster independent:
- Every test has its own keys.
QuoteId.New()per test and, where the tenant matters, an office of its own per test ("office-" + Guid.NewGuid()). - The clock is only ever set forward, and dates are chosen relative to it. Tests of one class run one after another and share the clock; a test that sets it forward by two days must not disturb another (lesson 4.5.7 showed how).
- One cluster per class with
IClassFixture. The start costs about a second; whoever has many small classes shares one cluster through a collection (ICollectionFixture). Then, however, all classes of the collection run one after another.