Edit

Share via


Unit testing with Orleans

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

Use the TestCluster

The Microsoft.Orleans.TestingHost NuGet package contains TestCluster, which you can use to create an in-memory cluster (comprised of two silos by default) for testing 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 might want to create a TestCluster and reuse it among multiple test cases. For example, achieve this 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);
    }
}

When all tests complete and the in-memory cluster silos stop, xUnit calls the Dispose() method of the ClusterFixture type. TestCluster also has a constructor accepting TestClusterOptions that you can use to configure the silos in the cluster.

If you use 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 allows mocking many parts of the system. For many scenarios, this is the easiest way to unit test grains. This approach has limitations (e.g., around scheduling reentrancy and serialization) and might require grains to include code used only by your unit tests. The Orleans TestKit provides an alternative approach that sidesteps many of these limitations.

For example, imagine the grain you're testing interacts with other grains. 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 enable mocking. So, the first step is to make GrainFactory both public and virtual:

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

Now you can create your grain outside 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, create the grain under test, WorkerGrain, using Moq. This allows overriding the GrainFactory's behavior so it returns a mocked IJournalGrain. You can then verify that WorkerGrain interacts with IJournalGrain as expected.