Unit testing with Orleans

This tutorial shows how to unit test your grains to make sure they behave correctly. There are two main ways to unit test your grains, and the method you choose will depend on the type of functionality you're testing. The Microsoft.Orleans.TestingHost NuGet package can be used to create test silos for your grains, or you can use a mocking framework like Moq to mock parts of the Orleans runtime that your grain interacts with.

Use the TestCluster

The Microsoft.Orleans.TestingHost NuGet package contains TestCluster which can be used to create an in-memory cluster, comprised of two silos by default, which can be used to test grains.

using Orleans.TestingHost;

namespace Tests;

public class HelloGrainTests
{
    [Fact]
    public async Task SaysHelloCorrectly()
    {
        var builder = new TestClusterBuilder();
        var cluster = builder.Build();
        cluster.Deploy();

        var hello = cluster.GrainFactory.GetGrain<IHelloGrain>(Guid.NewGuid());
        var greeting = await hello.SayHello("World");

        cluster.StopAllSilos();

        Assert.Equal("Hello, World!", greeting);
    }
}

Due to the overhead of starting an in-memory cluster, you may wish to create a TestCluster and reuse it among multiple test cases. For example, this can be done using xUnit's class or collection fixtures.

To share a TestCluster between multiple test cases, first create a fixture type:

using Orleans.TestingHost;

public sealed class ClusterFixture : IDisposable
{
    public TestCluster Cluster { get; } = new TestClusterBuilder().Build();

    public ClusterFixture() => Cluster.Deploy();

    void IDisposable.Dispose() => Cluster.StopAllSilos();
}

Next, create a collection fixture:

[CollectionDefinition(Name)]
public sealed class ClusterCollection : ICollectionFixture<ClusterFixture>
{
    public const string Name = nameof(ClusterCollection);
}

You can now reuse a TestCluster in your test cases:

using Orleans.TestingHost;

namespace Tests;

[Collection(ClusterCollection.Name)]
public class HelloGrainTestsWithFixture(ClusterFixture fixture)
{
    private readonly TestCluster _cluster = fixture.Cluster;

    [Fact]
    public async Task SaysHelloCorrectly()
    {
        var hello = _cluster.GrainFactory.GetGrain<IHelloGrain>(Guid.NewGuid());
        var greeting = await hello.SayHello("World");

        Assert.Equal("Hello, World!", greeting);
    }
}

xUnit calls the Dispose() method of the ClusterFixture type when all tests have been completed and the in-memory cluster silos are stopped. TestCluster also has a constructor that accepts TestClusterOptions that can be used to configure the silos in the cluster.

If you're using Dependency Injection in your Silo to make services available to Grains, you can use this pattern as well:

using Microsoft.Extensions.DependencyInjection;
using Orleans.TestingHost;

namespace Tests;

public sealed class ClusterFixtureWithConfig : IDisposable
{
    public TestCluster Cluster { get; } = new TestClusterBuilder()
        .AddSiloBuilderConfigurator<TestSiloConfigurations>()
        .Build();

    public ClusterFixtureWithConfig() => Cluster.Deploy();

    void IDisposable.Dispose() => Cluster.StopAllSilos();
}

file sealed class TestSiloConfigurations : ISiloConfigurator
{
    public void Configure(ISiloBuilder siloBuilder)
    {
        siloBuilder.ConfigureServices(static services =>
        {
            // TODO: Call required service registrations here.
            // services.AddSingleton<T, Impl>(/* ... */);
        });
    }
}

Use mocks

Orleans also makes it possible to mock many parts of the system, and for many scenarios, this is the easiest way to unit test grains. This approach does have limitations (for example, around scheduling reentrancy and serialization) and may require that grains include code used only by your unit tests. The Orleans TestKit provides an alternative approach, which side-steps many of these limitations.

For example, imagine that the grain you're testing interacts with other grains. To be able to mock those other grains, you also need to mock the GrainFactory member of the grain under test. By default GrainFactory is a normal protected property, but most mocking frameworks require properties to be public and virtual to be able to mock them. So the first thing you need to do is make GrainFactory both a public and virtual property:

public new virtual IGrainFactory GrainFactory
{
    get => base.GrainFactory;
}

Now you can create your grain outside of the Orleans runtime and use mocking to control the behavior of GrainFactory:

using Xunit;
using Moq;

namespace Tests;

public class WorkerGrainTests
{
    [Fact]
    public async Task RecordsMessageInJournal()
    {
        var data = "Hello, World";
        var journal = new Mock<IJournalGrain>();
        var worker = new Mock<WorkerGrain>();
        worker
            .Setup(x => x.GrainFactory.GetGrain<IJournalGrain>(It.IsAny<Guid>()))
            .Returns(journal.Object);

        await worker.DoWork(data)

        journal.Verify(x => x.Record(data), Times.Once());
    }
}

Here you create the grain under test, WorkerGrain, using Moq, which means you can override the behavior of the GrainFactory so that it returns a mocked IJournalGrain. You can then verify that the WorkerGrain interacts with the IJournalGrain as you expect.