단위 테스트는 최신 소프트웨어 개발 사례의 중요한 부분입니다. 단위 테스트는 비즈니스 논리 동작을 확인하고 향후 눈에 띄지 않는 호환성이 손상되는 변경을 도입하지 않도록 보호합니다. 지속성 함수는 복잡성이 쉽게 증가할 수 있으므로 단위 테스트를 도입하면 호환성이 손상되는 변경을 방지할 수 있습니다. 다음 섹션에서는 오케스트레이션 클라이언트, 오케스트레이터 및 작업 함수의 세 가지 함수 형식을 단위 테스트하는 방법을 설명합니다.
비고
이 문서에서는 .NET In process 작업자용 C#으로 작성된 Durable Functions 앱에 대한 단위 테스트 및 Durable Functions 2.x를 대상으로 하는 지침을 제공합니다. 버전 간의 차이점에 대한 자세한 내용은 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);
}
}
}
오케스트레이터 함수 단위 테스트
오케스트레이터 함수는 일반적으로 더 많은 비즈니스 논리를 가지고 있기 때문에 단위 테스트에 훨씬 더 흥미롭습니다.
이 섹션에서 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 액티비티 함수의 동작을 검증합니다.
// 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);
}
}
}