単体テストは、最新のソフトウェア開発プラクティスの重要な部分です。 単体テストでは、ビジネス ロジックの動作を検証し、将来目に見えない破壊的変更が発生しないように保護します。 Durable Functions は複雑さが増しやすいため、単体テストを導入すると、破壊的変更を回避できます。 次のセクションでは、オーケストレーション クライアント、オーケストレーター、アクティビティ関数の 3 つの関数の種類を単体テストする方法について説明します。
注
この記事では、.NET インプロセス ワーカー用に C# で記述され、Durable Functions 2.x を対象とする Durable Functions アプリの単体テストに関するガイダンスを提供します。 バージョン間の違いの詳細については、 Durable Functions のバージョン に関する記事を参照してください。
[前提条件]
この記事の例では、次の概念とフレームワークに関する知識が必要です。
モックオブジェクト作成のための基本クラス
モックは、次のインターフェイスを介してサポートされています。
これらのインターフェイスは、Durable Functions でサポートされているさまざまなトリガーとバインドと共に使用できます。 Azure Functions の実行中、関数ランタイムは、これらのインターフェイスの具体的な実装を使用して関数コードを実行します。 単体テストでは、これらのインターフェイスのモック バージョンを渡して、ビジネス ロジックをテストできます。
ユニットテスティングのトリガー関数
このセクションでは、単体テストで、新しいオーケストレーションを開始するための次の HTTP トリガー関数のロジックを検証します。
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
namespace VSSample
{
public static class HttpStart
{
[FunctionName("HttpStart")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
[DurableClient] IDurableClient starter,
string functionName,
ILogger log)
{
// Function input comes from the request content.
object eventData = await req.Content.ReadAsAsync<object>();
string instanceId = await starter.StartNewAsync(functionName, eventData);
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
return starter.CreateCheckStatusResponse(req, instanceId);
}
}
}
単体テスト タスクは、応答ペイロードで指定された Retry-After
ヘッダーの値を検証します。 そのため、単体テストでは、いくつかの IDurableClient
メソッドをモックして、予測可能な動作を保証します。
まず、モック フレームワーク (この場合は moq ) を使用して、 IDurableClient
をモックします。
// Mock IDurableClient
var durableClientMock = new Mock<IDurableClient>();
注
インターフェイスをクラスとして直接実装することでインターフェイスをモックできますが、モック フレームワークではさまざまな方法でプロセスが簡略化されます。 たとえば、マイナー リリース間で新しいメソッドがインターフェイスに追加された場合、moq では具体的な実装とは異なり、コードの変更は必要ありません。
次 StartNewAsync
メソッドは、既知のインスタンス ID を返すようにモックされます。
// Mock StartNewAsync method
durableClientMock.
Setup(x => x.StartNewAsync(functionName, It.IsAny<object>())).
ReturnsAsync(instanceId);
次の CreateCheckStatusResponse
は、常に空の HTTP 200 応答を返すようにモックされます。
// Mock CreateCheckStatusResponse method
durableClientMock
// Notice that even though the HttpStart function does not call IDurableClient.CreateCheckStatusResponse()
// with the optional parameter returnInternalServerErrorOnFailure, moq requires the method to be set up
// with each of the optional parameters provided. Simply use It.IsAny<> for each optional parameter
.Setup(x => x.CreateCheckStatusResponse(It.IsAny<HttpRequestMessage>(), instanceId, returnInternalServerErrorOnFailure: It.IsAny<bool>()))
.Returns(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(string.Empty),
Headers =
{
RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10))
}
});
ILogger
もモックされています。
// Mock ILogger
var loggerMock = new Mock<ILogger>();
これで、 Run
メソッドが単体テストから呼び出されます。
// Call Orchestration trigger function
var result = await HttpStart.Run(
new HttpRequestMessage()
{
Content = new StringContent("{}", Encoding.UTF8, "application/json"),
RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
},
durableClientMock.Object,
functionName,
loggerMock.Object);
最後の手順では、出力と期待される値を比較します。
// Validate that output is not null
Assert.NotNull(result.Headers.RetryAfter);
// Validate output's Retry-After header value
Assert.Equal(TimeSpan.FromSeconds(10), result.Headers.RetryAfter.Delta);
これらすべての手順を組み合わせた後、単体テストには次のコードがあります。
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
namespace VSSample.Tests
{
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http.Headers;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
public class HttpStartTests
{
[Fact]
public async Task HttpStart_returns_retryafter_header()
{
// Define constants
const string functionName = "SampleFunction";
const string instanceId = "7E467BDB-213F-407A-B86A-1954053D3C24";
// Mock TraceWriter
var loggerMock = new Mock<ILogger>();
// Mock DurableOrchestrationClientBase
var clientMock = new Mock<IDurableClient>();
// Mock StartNewAsync method
clientMock.
Setup(x => x.StartNewAsync(functionName, It.IsAny<string>(), It.IsAny<object>())).
ReturnsAsync(instanceId);
// Mock CreateCheckStatusResponse method
clientMock
.Setup(x => x.CreateCheckStatusResponse(It.IsAny<HttpRequestMessage>(), instanceId, false))
.Returns(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(string.Empty),
Headers =
{
RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10))
}
});
// Call Orchestration trigger function
var result = await HttpStart.Run(
new HttpRequestMessage()
{
Content = new StringContent("{}", Encoding.UTF8, "application/json"),
RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
},
clientMock.Object,
functionName,
loggerMock.Object);
// Validate that output is not null
Assert.NotNull(result.Headers.RetryAfter);
// Validate output's Retry-After header value
Assert.Equal(TimeSpan.FromSeconds(10), result.Headers.RetryAfter.Delta);
}
}
}
オーケストレーター関数の単体テスト
オーケストレーター関数は、通常、より多くのビジネス ロジックを持っているため、単体テストではさらに興味深いものです。
このセクションでは、単体テストで E1_HelloSequence
Orchestrator 関数の出力を検証します。
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
namespace VSSample
{
public static class HelloSequence
{
[FunctionName("E1_HelloSequence")]
public static async Task<List<string>> Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var outputs = new List<string>();
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Seattle"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello_DirectInput", "London"));
// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
return outputs;
}
[FunctionName("E1_SayHello")]
public static string SayHello([ActivityTrigger] IDurableActivityContext context)
{
string name = context.GetInput<string>();
return $"Hello {name}!";
}
[FunctionName("E1_SayHello_DirectInput")]
public static string SayHelloDirectInput([ActivityTrigger] string name)
{
return $"Hello {name}!";
}
}
}
単体テスト コードは、モックの作成から始まります。
var durableOrchestrationContextMock = new Mock<IDurableOrchestrationContext>();
その後、アクティビティ メソッドの呼び出しがモックされます。
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Tokyo")).ReturnsAsync("Hello Tokyo!");
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Seattle")).ReturnsAsync("Hello Seattle!");
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "London")).ReturnsAsync("Hello London!");
次に、単体テストで HelloSequence.Run
メソッドを呼び出します。
var result = await HelloSequence.Run(durableOrchestrationContextMock.Object);
最後に、出力が検証されます。
Assert.Equal(3, result.Count);
Assert.Equal("Hello Tokyo!", result[0]);
Assert.Equal("Hello Seattle!", result[1]);
Assert.Equal("Hello London!", result[2]);
前の手順を組み合わせた後、単体テストには次のコードがあります。
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
namespace VSSample.Tests
{
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Moq;
using Xunit;
public class HelloSequenceTests
{
[Fact]
public async Task Run_returns_multiple_greetings()
{
var mockContext = new Mock<IDurableOrchestrationContext>();
mockContext.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Tokyo")).ReturnsAsync("Hello Tokyo!");
mockContext.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Seattle")).ReturnsAsync("Hello Seattle!");
mockContext.Setup(x => x.CallActivityAsync<string>("E1_SayHello_DirectInput", "London")).ReturnsAsync("Hello London!");
var result = await HelloSequence.Run(mockContext.Object);
Assert.Equal(3, result.Count);
Assert.Equal("Hello Tokyo!", result[0]);
Assert.Equal("Hello Seattle!", result[1]);
Assert.Equal("Hello London!", result[2]);
}
}
}
単体テスト用アクティビティ関数
アクティビティ関数は、非持続性関数と同様の方法で単体テストされます。
このセクションでは、単体テストで E1_SayHello
Activity 関数の動作を検証します。
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
namespace VSSample
{
public static class HelloSequence
{
[FunctionName("E1_HelloSequence")]
public static async Task<List<string>> Run(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var outputs = new List<string>();
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Tokyo"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Seattle"));
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello_DirectInput", "London"));
// returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
return outputs;
}
[FunctionName("E1_SayHello")]
public static string SayHello([ActivityTrigger] IDurableActivityContext context)
{
string name = context.GetInput<string>();
return $"Hello {name}!";
}
[FunctionName("E1_SayHello_DirectInput")]
public static string SayHelloDirectInput([ActivityTrigger] string name)
{
return $"Hello {name}!";
}
}
}
単体テストでは、出力の形式を確認します。 これらの単体テストでは、パラメーター型を直接使用するか、クラス IDurableActivityContext
モックします。
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
namespace VSSample.Tests
{
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Xunit;
using Moq;
public class HelloSequenceActivityTests
{
[Fact]
public void SayHello_returns_greeting()
{
var durableActivityContextMock = new Mock<IDurableActivityContext>();
durableActivityContextMock.Setup(x => x.GetInput<string>()).Returns("John");
var result = HelloSequence.SayHello(durableActivityContextMock.Object);
Assert.Equal("Hello John!", result);
}
[Fact]
public void SayHello_returns_greeting_direct_input()
{
var result = HelloSequence.SayHelloDirectInput("John");
Assert.Equal("Hello John!", result);
}
}
}