使用 Orleans 执行单元测试

本教程演示了如何对粒度进行单元测试,以确保它们的行为正确。 可以采有两种主要方法对粒度进行单元测试,所选方法将取决于要测试的功能类型。 Microsoft.Orleans.TestingHost NuGet 包可用于为粒度创建测试接收器,也可以使用模拟框架(如 Moq)模拟粒度与之交互的 Orleans 运行时的某些部分。

使用 TestCluster

Microsoft.Orleans.TestingHost NuGet 包包含可用于创建内存中群集的 TestCluster,默认情况下由两个接收器组成,可用于测试粒度。

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

考虑到启动内存中群集的开销,你可能想要在多个测试用例之间创建 TestCluster 并进行重复使用。 例如,可以使用 xUnit 的类或集合固定例程来完成此操作。

若要在多个测试用例之间共享 TestCluster,请先创建固定例程类型:

using Orleans.TestingHost;

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

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

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

接下来,创建集合固定例程:

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

现在可以在测试用例中重复使用 TestCluster

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 将调用 ClusterFixture 类型的 Dispose() 方法,内存中群集接收器将被停用。 TestCluster 还有一个构造函数,该构造函数接受可用于在群集中配置接收器的 TestClusterOptions

如果在接收器中使用依赖关系注入来使服务可用于粒度,还可以使用此模式:

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

使用模拟

Orleans 还可以模拟系统的许多部分,对许多方案来说,这是对粒度执行单元测试的最简单方法。 此方法确实有一定的局限性(例如,关于进度安排可重入性和序列化),可能要求粒度包含仅供单元测试使用的代码。 Orleans TestKit 提供了一种替代方法,可对其中许多限制进行规避。

例如,假设你正在测试的粒度与其他粒度进行交互。 为了能够模拟这些其他粒度,你还需要模拟受试粒度的 GrainFactory 成员。 默认情况下,GrainFactory 是一个普通的 protected 属性,但大多数模拟框架都要求属性为 public 并且 virtual 能够模拟它们。 因此,你需要做的第一件事是使 GrainFactory 同时具有 publicvirtual 属性:

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

现在,你可以在 Orleans 运行时之外创建粒度,并使用模拟来控制 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());
    }
}

在这里,你将使用 Moq 创建要测试的粒度 WorkerGrain,这意味着你可以替代 GrainFactory 的行为,以便它返回模拟的 IJournalGrain。 然后,你可以验证 WorkerGrain 是否按预期方式与 IJournalGrain 进行交互。