使用Orleans进行单元测试

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

InProcessTestCluster是推荐用于Orleans的测试基础设施。 它提供了一个简化的基于委托的 API 来配置测试群集,从而更轻松地在测试与群集之间共享服务。

主要优势

InProcessTestCluster相比于TestCluster的主要优势是人体工学

  • 基于委托的配置:使用内联委托配置孤岛和客户端,而不是单独的配置类
  • 共享服务实例:在测试代码和独立主机之间轻松共享模拟服务、测试替身和其他实例
  • 更少的样板代码:无需创建单独的 ISiloConfigurator 类或 IClientConfigurator
  • 更简单的依赖项注入:直接在生成器 Fluent API 中注册服务

默认情况下,InProcessTestClusterTestCluster都使用相同的基础进程内仓库主机,因此内存使用和启动时间是相同的。 TestCluster该 API 还被设计用于支持多进程方案(适合生产环境的模拟),需要基于类的配置方法。然而,默认情况下,它的运行就像 InProcessTestCluster 一样是在进程内进行的。

基本用法

using Orleans.TestingHost;
using Xunit;

public class HelloGrainTests : IAsyncLifetime
{
    private InProcessTestCluster _cluster = null!;

    public async Task InitializeAsync()
    {
        var builder = new InProcessTestClusterBuilder();
        _cluster = builder.Build();
        await _cluster.DeployAsync();
    }

    public async Task DisposeAsync()
    {
        await _cluster.DisposeAsync();
    }

    [Fact]
    public async Task SaysHello()
    {
        var grain = _cluster.Client.GetGrain<IHelloGrain>(0);
        var result = await grain.SayHello("World");
        Assert.Equal("Hello, World!", result);
    }
}

配置测试群集

使用InProcessTestClusterBuilder来配置存储桶、客户端和服务:

var builder = new InProcessTestClusterBuilder(initialSilosCount: 2);

// Configure silos
builder.ConfigureSilo((options, siloBuilder) =>
{
    siloBuilder.AddMemoryGrainStorage("Default");
    siloBuilder.AddMemoryGrainStorage("PubSubStore");
});

// Configure clients
builder.ConfigureClient(clientBuilder =>
{
    // Client-specific configuration
});

// Configure both silos and clients (shared services)
builder.ConfigureHost(hostBuilder =>
{
    hostBuilder.Services.AddSingleton<IMyService, MyService>();
});

var cluster = builder.Build();
await cluster.DeployAsync();

InProcessTestClusterOptions

选项 类型 违约 Description
ClusterId string 自动生成 群集标识符。
ServiceId string 自动生成 服务标识符。
InitialSilosCount int 1 最初要启动的孤岛数。
InitializeClientOnDeploy bool true 是否在部署时自动初始化客户端。
ConfigureFileLogging bool true 启用用于调试的文件日志记录。
UseRealEnvironmentStatistics bool false 使用实际内存/CPU 统计信息,而不是模拟值。
GatewayPerSilo bool true 每个孤岛是否托管用于客户端连接的网关。

在测试之间共享测试群集

若要提高测试性能,请使用 xUnit 装置跨多个测试用例共享单个群集:

public class ClusterFixture : IAsyncLifetime
{
    public InProcessTestCluster Cluster { get; private set; } = null!;

    public async Task InitializeAsync()
    {
        var builder = new InProcessTestClusterBuilder();
        builder.ConfigureSilo((options, siloBuilder) =>
        {
            siloBuilder.AddMemoryGrainStorageAsDefault();
        });

        Cluster = builder.Build();
        await Cluster.DeployAsync();
    }

    public async Task DisposeAsync()
    {
        await Cluster.DisposeAsync();
    }
}

[CollectionDefinition(nameof(ClusterCollection))]
public class ClusterCollection : ICollectionFixture<ClusterFixture>
{
}

[Collection(nameof(ClusterCollection))]
public class HelloGrainTests
{
    private readonly ClusterFixture _fixture;

    public HelloGrainTests(ClusterFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task SaysHello()
    {
        var grain = _fixture.Cluster.Client.GetGrain<IHelloGrain>(0);
        var result = await grain.SayHello("World");
        Assert.Equal("Hello, World!", result);
    }
}

在测试期间添加和删除孤岛

支持 InProcessTestCluster 进行动态孤岛管理,以测试群集行为。

// Start with 2 silos
var builder = new InProcessTestClusterBuilder(initialSilosCount: 2);
var cluster = builder.Build();
await cluster.DeployAsync();

// Add a third silo
var newSilo = await cluster.StartSiloAsync();

// Stop a silo
await cluster.StopSiloAsync(newSilo);

// Restart all silos
await cluster.RestartAsync();

使用 TestCluster

基于类的配置方法需要使用 TestCluster 并实现 ISiloConfiguratorIClientConfigurator 接口。 此设计支持多进程测试场景,其中孤岛在单独的进程中运行,这对于生产环境模拟测试非常有用。 但是,默认情况下,TestCluster 还会以与 InProcessTestCluster 等效的性能运行在进程内。

在以下情况下选择TestCluster而不是InProcessTestCluster

  • 需要对生产模拟进行多进程测试
  • 已有使用 TestCluster API 的测试
  • 需要与 Orleans 7.x 或 8.x 兼容

对于新测试,建议使用 InProcessTestCluster,因为它提供了更简单的基于委托的配置。

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)
    {
        // TODO: Call required service registrations here.
        // siloBuilder.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 按预期交互。