本教程演示如何对粒度进行单元测试,以确保它们的行为正确。 有两种主要方法可以对粒度进行单元测试,你选择的方法取决于要测试的功能类型。 使用 Microsoft.Orleans.TestingHost NuGet 包为 grain 创建测试仓,或使用模拟框架(如 Moq)模拟 grain 与之交互的 Orleans 运行时部分。
使用 InProcessTestCluster(推荐)
InProcessTestCluster是推荐用于Orleans的测试基础设施。 它提供了一个简化的基于委托的 API 来配置测试群集,从而更轻松地在测试与群集之间共享服务。
主要优势
InProcessTestCluster相比于TestCluster的主要优势是人体工学:
- 基于委托的配置:使用内联委托配置孤岛和客户端,而不是单独的配置类
- 共享服务实例:在测试代码和独立主机之间轻松共享模拟服务、测试替身和其他实例
-
更少的样板代码:无需创建单独的
ISiloConfigurator类或IClientConfigurator类 - 更简单的依赖项注入:直接在生成器 Fluent API 中注册服务
默认情况下,InProcessTestCluster和TestCluster都使用相同的基础进程内仓库主机,因此内存使用和启动时间是相同的。
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 并实现 ISiloConfigurator 和 IClientConfigurator 接口。 此设计支持多进程测试场景,其中孤岛在单独的进程中运行,这对于生产环境模拟测试非常有用。 但是,默认情况下,TestCluster 还会以与 InProcessTestCluster 等效的性能运行在进程内。
在以下情况下选择TestCluster而不是InProcessTestCluster:
- 需要对生产模拟进行多进程测试
- 已有使用
TestClusterAPI 的测试 - 需要与 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 按预期交互。