共用方式為


使用 Orleans 進行單元測試

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

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

主要優勢

相較於 TestClusterInProcessTestCluster 的主要優點是人體工學

  • 基於委託式配置:使用內嵌代理配置孤島與用戶端,而非獨立的組態類別
  • 共享服務實例:輕鬆在測試程式碼與孤島主機間共享模擬服務、測試雙重實例及其他實例
  • 減少樣板程式碼:不需要建立獨立的 ISiloConfiguratorIClientConfigurator 類別。
  • 更簡單的依賴注入:直接在 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 採用類別為基礎的配置方法,需要實作 ISiloConfiguratorIClientConfigurator 介面。 此設計支援多程序測試情境,讓獨立的程序分別運行,對類生產環境的模擬測試非常有幫助。 然而,預設 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() 類型的方法 ClusterFixtureTestCluster 也有一個可以接受 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 屬性,但大部分的模擬架構都需要屬性為 publicvirtual 才能啟用模擬。 因此,第一個步驟是使 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 互動。