次の方法で共有


を使用した単体テスト Orleans

このチュートリアルでは、グレインを単体テストして、正しく動作することを確認する方法について説明します。 グレインを単体テストする主な方法は 2 つあります。選択する方法は、テストする機能の種類によって異なります。 Microsoft.Orleans.TestingHost NuGet パッケージを使用して、グレインのテストサイロを作成するか、Moq などのモックフレームワークを使用して、グレインが対話する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

Option タイプ 既定値 Description
ClusterId string 自動生成 クラスター識別子。
ServiceId string 自動生成 サービス識別子。
InitialSilosCount int 1 最初に開始するサイロの数。
InitializeClientOnDeploy bool true デプロイ時にクライアントを自動初期化するかどうかを指定します。
ConfigureFileLogging bool true デバッグ用のファイル ログを有効にします。
UseRealEnvironmentStatistics bool false シミュレートされた値の代わりに、実際のメモリ/CPU 統計を使用します。
GatewayPerSilo bool true 各サイロがクライアント接続用のゲートウェイをホストするかどうか。

テスト間でテスト クラスターを共有する

テスト パフォーマンスを向上させるには、xUnit フィクスチャを使用して複数のテスト ケース間で 1 つのクラスターを共有します。

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 を使用して既存のテストがある
  • Orleans 7.x または 8.x との互換性が必要です

新しいテストでは、デリゲート ベースの構成が単純なため、 InProcessTestCluster をお勧めします。

Microsoft.Orleans.TestingHost NuGet パッケージにはTestClusterが含まれています。これは、グレインをテストするためのメモリ内クラスター (既定では 2 つのサイロで構成) を作成するために使用できます。

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 を受け入れるコンストラクターもあります。

サイロで依存性注入を用いてサービスを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 また、システムの多くの部分をモックすることができます。 多くのシナリオでは、これは、グレインを単体テストする最も簡単な方法です。 この方法には限界があります(たとえば、再入可能性とシリアル化に関するスケジュール設定など)で、単体テストでのみ使用されるコードを含める必要がある場合があります。 Orleans TestKit には、これらの制限の多くを回避する代替アプローチが用意されています。

たとえば、テストしているグレインが他のグレインと対話するとします。 これらの他のグレインをモックするには、テスト対象のグレインの GrainFactory メンバーをモックする必要もあります。 既定では、GrainFactory は通常のprotectedプロパティですが、ほとんどのモックフレームワークでは、モックを可能にするためにプロパティをpublicし、virtualにする必要があります。 そのため、最初の手順は、GrainFactorypublicの両方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 と対話することを確認できます。