使用 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
同时具有 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
进行交互。