使用 Orleans 進行單元測試

本教學課程將示範如何對您的 grain 進行單元測試,以確保它們能正常運作。 對元件進行單元測試有兩個主要方法,而選擇哪一種方法取決於您要測試的功能類型。 使用 Microsoft。Orleans。TestingHost NuGet 套件可為您的粒子建立測試沙洛,或使用 Moq 之類的模擬框架來模擬您粒子互動的運行時環境部分。

InProcessTestCluster 是推薦的 Orleans 測試基礎設施。 它提供一個簡化、以委托為基礎的 API,用於配置測試叢集,讓測試與叢集間的服務共享更為方便。

主要優勢

相較於 TestCluster,InProcessTestCluster 的主要優點是人體工學。

  • 基於委託式配置:使用內嵌代理配置孤島與用戶端,而非獨立的組態類別
  • 共享服務實例:輕鬆在測試程式碼與孤島主機間共享模擬服務、測試雙重實例及其他實例
  • 減少樣板程式碼:不需要建立獨立的 ISiloConfigurator 或 IClientConfigurator 類別。
  • 更簡單的依賴注入:直接在 builder fluent API 中註冊服務

兩者InProcessTestClusterTestCluster預設使用相同的底層進程孤島主機,因此記憶體使用量和啟動時間是等效的。 API TestCluster 也被設計用於支援多程序情境(用於類似生產環境的模擬),這需要基於類別的配置方法,但預設情況下它像 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

Option 類型 預設 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時

  • 生產模擬需要多流程測試
  • 你已經有使用 TestCluster API 進行的測試
  • 你需要與 7.x 或 8.x 相容Orleans

對於新測試 InProcessTestCluster ,因其代理式配置較簡單而被推薦。

Microsoft.Orleans.TestingHost NuGet 套件包含 TestCluster,可用來建立記憶體中的叢集(預設由兩個 silo 組成),以測試粒子。

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中使用相依性插入來讓服務可供穀粒使用,您也可以使用這種模式:

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 互動。