使用Orleans进行单元测试

本教程演示如何对粒度进行单元测试,以确保它们的行为正确。 有两种主要方法可以对粒度进行单元测试,你选择的方法取决于要测试的功能类型。 使用 Microsoft.Orleans.TestingHost NuGet 包为 grain 创建测试仓,或使用模拟框架(如 Moq)模拟 grain 与之交互的 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 将调用 Dispose() 类型的 ClusterFixture 方法。 TestCluster 还具有一个构造函数,该构造函数接受 TestClusterOptions,可用于配置集群中的独立单元。

如果在 Silo 中使用依赖注入使服务可用于 Grains,您也可以使用这种模式:

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 还允许对系统的许多部分进行模拟。 对于许多方案,这是单元测试粒度的最简单方法。 此方法有其局限性(例如,涉及到计划的再进入和序列化),并且可能需要grains中包含仅供单元测试使用的代码。 Orleans TestKit 提供了一种替代方法,可避开其中许多限制。

例如,假设要测试的粒度与其他粒度进行交互。 要模拟其他粒度,还需要模拟GrainFactory 被测试的粒度的成员。 默认情况下,GrainFactory 是一个普通 protected 属性,但大多数模拟框架要求属性必须是 public 并且 virtual 才能启用模拟。 因此,第一步是使GrainFactory既是public又是virtual

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 按预期交互。