Durable Functions および Durable Task SDK の単体テストを行う

単体テストの持続的オーケストレーションは、ビジネス ロジックを検証し、早期にエラーをキャッチするのに役立ちます。 オーケストレーションは複数のアクティビティを調整し、複雑になる可能性があるため、ワークフローの進化に合わせてテストが回帰から保護されます。

Azure Functionsを使用する場合は Durable Functions、Azure Functionsなしでスタンドアロン SDK を使用する場合は < >c1>Durable Task SDK プロジェクトに一致するタブを選択します。

Durable Functionsでは、オーケストレーター、アクティビティ、およびクライアント (トリガー) 関数をテストするには、フレームワークによって提供されるコンテキスト オブジェクトを直接呼び出します。 この方法では、ビジネス ロジックが Azure Functions ランタイムから分離されます。

パターンを示す最小限の C# オーケストレーター テストを次に示します。

[Fact]
public async Task MyOrchestrator_CallsActivity()
{
    var contextMock = new Mock<TaskOrchestrationContext>();
    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.IsAny<TaskName>(), It.IsAny<string>(), It.IsAny<TaskOptions>()))
        .ReturnsAsync("result");

    var result = await MyOrchestrator.Run(contextMock.Object);

    Assert.Equal("result", result);
}

この記事の残りの部分では、C# とPythonのこのパターンについて詳しく説明します。

スタンドアロンの Durable Task SDK は、外部の依存関係なしでメモリ内でオーケストレーションを実行する 組み込みのテスト インフラストラクチャ を提供します。 オーケストレーターとアクティビティをテスト ワーカーに登録し、テスト クライアントを使用してオーケストレーションをスケジュールし、結果をアサートします。 C# と JavaScript にモックは必要ありません。 Pythonでは、手動の結果挿入でジェネレーター ベースのアプローチを使用します。

パターンを示す最小限の C# テストを次に示します。

[Fact]
public async Task MyOrchestrator_Completes()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<MyOrchestrator>();
        tasks.AddActivity<MyActivity>();
    });

    string id = await host.Client.ScheduleNewOrchestrationInstanceAsync(nameof(MyOrchestrator));
    var result = await host.Client.WaitForInstanceCompletionAsync(id, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, result.RuntimeStatus);
}

この記事の残りの部分では、C#、Python、JavaScript のこのパターンについて詳しく説明します。

前提条件

オーケストレーター関数をテストする

オーケストレーター関数は、アクティビティ、タイマー、および外部イベントを調整します。 通常は、最も多くのビジネス ロジックが含まれており、単体テストのメリットが最も高まります。

オーケストレーション コンテキストをモックして、アクティビティ呼び出しの戻り値を制御します。 次に、オーケストレーターを直接呼び出し、出力を確認します。

アクティビティを 3 回呼び出すこのオーケストレーターについて考えてみましょう。

[Function(nameof(HelloCitiesOrchestration))]
public static async Task<List<string>> HelloCities(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var outputs = new List<string>
    {
        await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo"),
        await context.CallActivityAsync<string>(nameof(SayHello), "Seattle"),
        await context.CallActivityAsync<string>(nameof(SayHello), "London")
    };

    return outputs;
}

Moq を使用して TaskOrchestrationContext をモックし、各アクティビティ呼び出しに対して期待される戻り値を設定します。

Note

It.Is<TaskName>(...)はプレーン文字列ではなくCallActivityAsync構造体を受け入れるため、TaskName パターンが必要です。 Moq には、明示された型の一致が必要です。

[Fact]
public async Task HelloCities_ReturnsExpectedGreetings()
{
    var contextMock = new Mock<TaskOrchestrationContext>();

    // Mock each activity call to return a known value
    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.Is<TaskName>(n => n.Name == nameof(SayHello)),
        It.Is<string>(n => n == "Tokyo"),
        It.IsAny<TaskOptions>())).ReturnsAsync("Hello Tokyo!");

    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.Is<TaskName>(n => n.Name == nameof(SayHello)),
        It.Is<string>(n => n == "Seattle"),
        It.IsAny<TaskOptions>())).ReturnsAsync("Hello Seattle!");

    contextMock.Setup(x => x.CallActivityAsync<string>(
        It.Is<TaskName>(n => n.Name == nameof(SayHello)),
        It.Is<string>(n => n == "London"),
        It.IsAny<TaskOptions>())).ReturnsAsync("Hello London!");

    var result = await HelloCitiesOrchestration.HelloCities(contextMock.Object);

    Assert.Equal(3, result.Count);
    Assert.Equal("Hello Tokyo!", result[0]);
    Assert.Equal("Hello Seattle!", result[1]);
    Assert.Equal("Hello London!", result[2]);
}

DurableTaskTestHostを使用して、メモリ内でオーケストレーションを実行します。 運用オーケストレーターとアクティビティ クラスを登録し、オーケストレーションをスケジュールし、その結果を確認します。

次の実稼働クラスが与えられている場合:

class HelloCitiesOrchestrator : TaskOrchestrator<string, List<string>>
{
    public override async Task<List<string>> RunAsync(
        TaskOrchestrationContext context, string input)
    {
        var outputs = new List<string>
        {
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "Tokyo"),
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "Seattle"),
            await context.CallActivityAsync<string>(nameof(SayHelloActivity), "London")
        };
        return outputs;
    }
}

class SayHelloActivity : TaskActivity<string, string>
{
    public override Task<string> RunAsync(TaskActivityContext context, string name)
    {
        return Task.FromResult($"Hello {name}!");
    }
}

テスト ホストに直接登録します。

[Fact]
public async Task HelloCities_ReturnsExpectedGreetings()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<HelloCitiesOrchestrator>();
        tasks.AddActivity<SayHelloActivity>();
    });

    string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestrator));
    OrchestrationMetadata result = await host.Client.WaitForInstanceCompletionAsync(
        instanceId, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, result.RuntimeStatus);

    var output = result.ReadOutputAs<List<string>>();
    Assert.Equal(3, output.Count);
    Assert.Equal("Hello Tokyo!", output[0]);
    Assert.Equal("Hello Seattle!", output[1]);
    Assert.Equal("Hello London!", output[2]);
}

DurableTaskTestHost は、完全なメモリ内オーケストレーション エンジンを実行します。 外部サービスやサイドカー プロセスは必要ありません。

アクティビティ関数をテストする

アクティビティ関数には、API の呼び出し、データの処理、外部システムとの対話など、実際の作業が含まれます。 フレームワーク固有の再生動作がないため、テストする最も簡単な関数型です。

Azure Functionsのアクティビティ関数は、入力と必要に応じて FunctionContext を受け取ります。 他の関数と同様にテストします。

[Function(nameof(SayHello))]
public static string SayHello(
    [ActivityTrigger] string name, FunctionContext executionContext)
{
    return $"Hello {name}!";
}
[Fact]
public void SayHello_ReturnsExpectedGreeting()
{
    var result = HelloCitiesOrchestration.SayHello("Tokyo", Mock.Of<FunctionContext>());
    Assert.Equal("Hello Tokyo!", result);
}

アクティビティ関数は、コンテキスト オブジェクトと入力を受け取ります。 コンテキストはオーケストレーション ID やタスク ID などのメタデータを提供しますが、ほとんどのテストでは必要ありません。

オーケストレーターの例の SayHelloActivity クラスを使用して、モック コンテキストを使用して RunAsync を直接呼び出します。

[Fact]
public async Task SayHello_ReturnsExpectedGreeting()
{
    var activity = new SayHelloActivity();
    var contextMock = new Mock<TaskActivityContext>();

    var result = await activity.RunAsync(contextMock.Object, "Tokyo");

    Assert.Equal("Hello Tokyo!", result);
}

DurableTaskTestHostを使用すると、アクティビティもオーケストレーション テストの一部として実行されます。 アクティビティに複雑なロジックがない限り、個別のアクティビティ テストは必要ありません。

クライアント関数をテストする

クライアント関数 (トリガー関数とも呼ばれます) はオーケストレーションを開始し、インスタンスを管理します。 永続的なクライアント バインドを使用してオーケストレーション エンジンと対話します。

オーケストレーションを開始する次の HTTP トリガーについて考えてみましょう。

[Function("HelloCitiesOrchestration_HttpStart")]
public static async Task<HttpResponseData> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    [DurableClient] DurableTaskClient client,
    FunctionContext executionContext)
{
    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestration));
    return await client.CreateCheckStatusResponseAsync(req, instanceId);
}

モック DurableTaskClient を使用して既知のインスタンス ID を返す。

[Fact]
public async Task HttpStart_ReturnsAccepted()
{
    var durableClientMock = new Mock<DurableTaskClient>("testClient");
    var functionContextMock = new Mock<FunctionContext>();
    var instanceId = "test-instance-id";

    durableClientMock
        .Setup(x => x.ScheduleNewOrchestrationInstanceAsync(
            It.IsAny<TaskName>(),
            It.IsAny<object>(),
            It.IsAny<StartOrchestrationOptions>(),
            It.IsAny<CancellationToken>()))
        .ReturnsAsync(instanceId);

    var mockRequest = CreateMockHttpRequest(functionContextMock.Object);

    var responseMock = new Mock<HttpResponseData>(functionContextMock.Object);
    responseMock.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.Accepted);

    durableClientMock
        .Setup(x => x.CreateCheckStatusResponseAsync(
            It.IsAny<HttpRequestData>(),
            It.IsAny<string>(),
            It.IsAny<CancellationToken>()))
        .ReturnsAsync(responseMock.Object);

    var result = await HelloCitiesOrchestration.HttpStart(
        mockRequest, durableClientMock.Object, functionContextMock.Object);

    Assert.Equal(HttpStatusCode.Accepted, result.StatusCode);
}

クライアント操作をテストする

スタンドアロンの Durable Task SDK では、クライアント操作 (オーケストレーションのスケジュール設定、状態の照会、イベントの発生) では、オーケストレーター テストに既に示されているのと同じ TestOrchestrationClient が使用されます。 別のクライアント関数は存在しません。クライアント API を直接呼び出します。

DurableTaskTestHostは、完全に機能するhost.ClientであるDurableTaskClientを公開します。 これを使用して、オーケストレーションのスケジュール、クエリ、終了などのクライアント レベルの操作をテストします。

[Fact]
public async Task Client_CanQueryOrchestrationStatus()
{
    await using var host = await DurableTaskTestHost.StartAsync(tasks =>
    {
        tasks.AddOrchestrator<HelloCitiesOrchestrator>();
        tasks.AddActivity<SayHelloActivity>();
    });

    string instanceId = await host.Client.ScheduleNewOrchestrationInstanceAsync(
        nameof(HelloCitiesOrchestrator));

    // Query status while the orchestration runs
    OrchestrationMetadata metadata = await host.Client.WaitForInstanceCompletionAsync(
        instanceId, getInputsAndOutputs: true);

    Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus);
    Assert.Equal(instanceId, metadata.InstanceId);
}