Skip to content

Commit a5e8fa0

Browse files
authored
Merge pull request #301649 from nytian/unit-test-isolated
Add Unit Test Guide for Durable Functions .NET Isolated
2 parents 6ce0954 + 1aec5e1 commit a5e8fa0

File tree

3 files changed

+311
-2
lines changed

3 files changed

+311
-2
lines changed

articles/azure-functions/durable/TOC.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,10 @@
188188
href: durable-functions-create-portal.md
189189
- name: Manage connections
190190
href: ../manage-connections.md?toc=/azure/azure-functions/durable/toc.json
191-
- name: Unit testing (C#)
191+
- name: Unit testing (C# in-process)
192192
href: durable-functions-unit-testing.md
193+
- name: Unit testing (C# isolated)
194+
href: durable-functions-unit-testing-dotnet-isolated.md
193195
- name: Unit testing (Python)
194196
href: durable-functions-unit-testing-python.md
195197
- name: Create as WebJobs
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
---
2+
title: Unit Testing Durable Functions in .NET Isolated
3+
description: Learn about unit testing best practices for Durable functions written in .NET Isolated for Azure Functions.
4+
author: nytian
5+
ms.topic: conceptual
6+
ms.date: 05/19/2025
7+
ms.author: azfuncdf
8+
---
9+
10+
# Durable Functions unit testing (C# Isolated)
11+
12+
Unit testing is an important part of modern software development practices. Unit tests verify business logic behavior and protect from introducing unnoticed breaking changes in the future. Durable Functions can easily grow in complexity so introducing unit tests helps avoid breaking changes. The following sections explain how to unit test the three function types - Orchestration client, orchestrator, and activity functions.
13+
14+
> [!NOTE]
15+
> - This article provides guidance for unit testing for Durable Functions apps written in C# for the .NET isolated worker. For more information about Durable Functions in the .NET isolated worker, see the [Durable Functions in the .NET isolated worker](durable-functions-dotnet-isolated-overview.md) article.
16+
> - **The complete sample code for this unit testing guide can be found in [the sample code repository](https://github.com/Azure/azure-functions-durable-extension/tree/dev/samples/isolated-unit-tests)**.
17+
> - For Durable Functions using C# in-process, refer to [unit testing guide](durable-functions-unit-testing.md).
18+
19+
## Prerequisites
20+
21+
The examples in this article require knowledge of the following concepts and frameworks:
22+
23+
* Unit testing
24+
* Durable Functions
25+
* [xUnit](https://github.com/xunit/xunit) - Testing framework
26+
* [moq](https://github.com/moq/moq4) - Mocking framework
27+
28+
## Base classes for mocking
29+
30+
Mocking is supported via the following interfaces and classes:
31+
32+
* `DurableTaskClient` - For orchestrator client operations
33+
* `TaskOrchestrationContext` - For orchestrator function execution
34+
* `FunctionContext` - For function execution context
35+
* `HttpRequestData` and `HttpResponseData` - For HTTP trigger functions
36+
37+
These classes can be used with the various trigger and bindings supported by Durable Functions. While it's executing your Azure Functions, the functions runtime runs your function code with concrete implementations of these classes. For unit testing, you can pass in a mocked version of these classes to test your business logic.
38+
39+
## Unit testing trigger functions
40+
41+
In this section, the unit test validates the logic of the following HTTP trigger function for starting new orchestrations.
42+
43+
```csharp
44+
[Function("HelloCitiesOrchestration_HttpStart")]
45+
public static async Task<HttpResponseData> HttpStart(
46+
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
47+
[DurableClient] DurableTaskClient client,
48+
FunctionContext executionContext)
49+
{
50+
// Function input comes from the request content.
51+
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
52+
nameof(HelloCitiesOrchestration));
53+
54+
// Returns an HTTP 202 response with an instance management payload.
55+
return await client.CreateCheckStatusResponseAsync(req, instanceId);
56+
}
57+
```
58+
59+
The unit test verifies the HTTP response status code and the instance ID in the response payload. The test mocks the `DurableTaskClient` to ensure predictable behavior.
60+
61+
First, we use a mocking framework (Moq in this case) to mock `DurableTaskClient` and FunctionContext:
62+
63+
```csharp
64+
// Mock DurableTaskClient
65+
var durableClientMock = new Mock<DurableTaskClient>("testClient");
66+
var functionContextMock = new Mock<FunctionContext>();
67+
```
68+
69+
Then `ScheduleNewOrchestrationInstanceAsync` method is mocked to return an instance ID:
70+
71+
```csharp
72+
var instanceId = Guid.NewGuid().ToString();
73+
74+
// Mock ScheduleNewOrchestrationInstanceAsync method
75+
durableClientMock
76+
.Setup(x => x.ScheduleNewOrchestrationInstanceAsync(
77+
It.IsAny<TaskName>(),
78+
It.IsAny<object>(),
79+
It.IsAny<StartOrchestrationOptions>(),
80+
It.IsAny<CancellationToken>()))
81+
.ReturnsAsync(instanceId);
82+
```
83+
84+
Next, we need to mock the HTTP request and response data:
85+
86+
```csharp
87+
// Mock HttpRequestData that sent to the http trigger
88+
var mockRequest = MockHttpRequestAndResponseData();
89+
90+
var responseMock = new Mock<HttpResponseData>(functionContextMock.Object);
91+
responseMock.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.Accepted);
92+
```
93+
94+
Here's the complete helper method for mocking HttpRequestData:
95+
96+
```csharp
97+
private HttpRequestData MockHttpRequestAndResponseData(HttpHeadersCollection? headers = null)
98+
{
99+
var mockObjectSerializer = new Mock<ObjectSerializer>();
100+
101+
// Setup the SerializeAsync method
102+
mockObjectSerializer.Setup(s => s.SerializeAsync(It.IsAny<Stream>(), It.IsAny<object?>(), It.IsAny<Type>(), It.IsAny<CancellationToken>()))
103+
.Returns<Stream, object?, Type, CancellationToken>(async (stream, value, type, token) =>
104+
{
105+
await System.Text.Json.JsonSerializer.SerializeAsync(stream, value, type, cancellationToken: token);
106+
});
107+
108+
var workerOptions = new WorkerOptions
109+
{
110+
Serializer = mockObjectSerializer.Object
111+
};
112+
var mockOptions = new Mock<IOptions<WorkerOptions>>();
113+
mockOptions.Setup(o => o.Value).Returns(workerOptions);
114+
115+
// Mock the service provider
116+
var mockServiceProvider = new Mock<IServiceProvider>();
117+
118+
// Set up the service provider to return the mock IOptions<WorkerOptions>
119+
mockServiceProvider.Setup(sp => sp.GetService(typeof(IOptions<WorkerOptions>)))
120+
.Returns(mockOptions.Object);
121+
122+
// Set up the service provider to return the mock ObjectSerializer
123+
mockServiceProvider.Setup(sp => sp.GetService(typeof(ObjectSerializer)))
124+
.Returns(mockObjectSerializer.Object);
125+
126+
// Create a mock FunctionContext and assign the service provider
127+
var mockFunctionContext = new Mock<FunctionContext>();
128+
mockFunctionContext.SetupGet(c => c.InstanceServices).Returns(mockServiceProvider.Object);
129+
var mockHttpRequestData = new Mock<HttpRequestData>(mockFunctionContext.Object);
130+
131+
// Set up the URL property
132+
mockHttpRequestData.SetupGet(r => r.Url).Returns(new Uri("https://localhost:7075/orchestrators/HelloCities"));
133+
134+
// If headers are provided, use them, otherwise create a new empty HttpHeadersCollection
135+
headers ??= new HttpHeadersCollection();
136+
137+
// Setup the Headers property to return the empty headers
138+
mockHttpRequestData.SetupGet(r => r.Headers).Returns(headers);
139+
140+
var mockHttpResponseData = new Mock<HttpResponseData>(mockFunctionContext.Object)
141+
{
142+
DefaultValue = DefaultValue.Mock
143+
};
144+
145+
// Enable setting StatusCode and Body as mutable properties
146+
mockHttpResponseData.SetupProperty(r => r.StatusCode, HttpStatusCode.OK);
147+
mockHttpResponseData.SetupProperty(r => r.Body, new MemoryStream());
148+
149+
// Setup CreateResponse to return the configured HttpResponseData mock
150+
mockHttpRequestData.Setup(r => r.CreateResponse())
151+
.Returns(mockHttpResponseData.Object);
152+
153+
return mockHttpRequestData.Object;
154+
}
155+
```
156+
157+
Finally, we call the function and verify the results:
158+
159+
```csharp
160+
var result = await HelloCitiesOrchestration.HttpStart(mockRequest, durableClientMock.Object, functionContextMock.Object);
161+
162+
// Verify the status code
163+
Assert.Equal(HttpStatusCode.Accepted, result.StatusCode);
164+
165+
// Reset stream position for reading
166+
result.Body.Position = 0;
167+
var serializedResponseBody = await System.Text.Json.JsonSerializer.DeserializeAsync<dynamic>(result.Body);
168+
169+
// Verify the response returned contains the right data
170+
Assert.Equal(instanceId, serializedResponseBody!.GetProperty("Id").GetString());
171+
```
172+
173+
> [!NOTE]
174+
> Currently, loggers created via FunctionContext in trigger functions aren't supported for mocking in unit tests.
175+
176+
177+
## Unit testing orchestrator functions
178+
179+
Orchestrator functions manage the execution of multiple activity functions. To test an orchestrator:
180+
181+
* Mock the `TaskOrchestrationContext` to control function execution
182+
* Replace `TaskOrchestrationContext` methods needed for orchestrator execution like `CallActivityAsync` with mock functions
183+
* Call the orchestrator directly with the mocked context
184+
* Verify the orchestrator results using assertions
185+
186+
In this section, the unit test validates the behavior of the `HelloCities` orchestrator function:
187+
188+
```csharp
189+
[Function(nameof(HelloCitiesOrchestration))]
190+
public static async Task<List<string>> HelloCities(
191+
[OrchestrationTrigger] TaskOrchestrationContext context)
192+
{
193+
ILogger logger = context.CreateReplaySafeLogger(nameof(HelloCities));
194+
logger.LogInformation("Saying hello.");
195+
var outputs = new List<string>();
196+
197+
outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Tokyo"));
198+
outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "Seattle"));
199+
outputs.Add(await context.CallActivityAsync<string>(nameof(SayHello), "London"));
200+
201+
return outputs;
202+
}
203+
```
204+
205+
The unit test verifies that the orchestrator calls the correct activity functions with the expected parameters and returns the expected results. The test mocks the `TaskOrchestrationContext` to ensure predictable behavior.
206+
207+
First, we use a mocking framework (Moq in this case) to mock `TaskOrchestrationContext`:
208+
209+
```csharp
210+
// Mock TaskOrchestrationContext and setup logger
211+
var contextMock = new Mock<TaskOrchestrationContext>();
212+
```
213+
214+
Then we mock the `CreateReplaySafeLogger` method to return our test logger:
215+
216+
```csharp
217+
testLogger = new Mock<ILogger>();
218+
contextMock.Setup(x => x.CreateReplaySafeLogger(It.IsAny<string>()))
219+
.Returns(testLogger.Object);
220+
```
221+
222+
Next, we mock the activity function calls with specific return values for each city:
223+
224+
```csharp
225+
// Mock the activity function calls
226+
contextMock.Setup(x => x.CallActivityAsync<string>(
227+
It.Is<TaskName>(n => n.Name == nameof(HelloCitiesOrchestration.SayHello)),
228+
It.Is<string>(n => n == "Tokyo"),
229+
It.IsAny<TaskOptions>()))
230+
.ReturnsAsync("Hello Tokyo!");
231+
contextMock.Setup(x => x.CallActivityAsync<string>(
232+
It.Is<TaskName>(n => n.Name == nameof(HelloCitiesOrchestration.SayHello)),
233+
It.Is<string>(n => n == "Seattle"),
234+
It.IsAny<TaskOptions>()))
235+
.ReturnsAsync("Hello Seattle!");
236+
contextMock.Setup(x => x.CallActivityAsync<string>(
237+
It.Is<TaskName>(n => n.Name == nameof(HelloCitiesOrchestration.SayHello)),
238+
It.Is<string>(n => n == "London"),
239+
It.IsAny<TaskOptions>()))
240+
.ReturnsAsync("Hello London!");
241+
```
242+
243+
Then we call the orchestrator function with the mocked context:
244+
245+
```csharp
246+
var result = await HelloCitiesOrchestration.HelloCities(contextMock.Object);
247+
```
248+
249+
Finally, we verify the orchestration result and logging behavior:
250+
251+
```csharp
252+
// Verify the orchestration result
253+
Assert.Equal(3, result.Count);
254+
Assert.Equal("Hello Tokyo!", result[0]);
255+
Assert.Equal("Hello Seattle!", result[1]);
256+
Assert.Equal("Hello London!", result[2]);
257+
258+
// Verify logging
259+
testLogger.Verify(
260+
x => x.Log(
261+
It.Is<LogLevel>(l => l == LogLevel.Information),
262+
It.IsAny<EventId>(),
263+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Saying hello")),
264+
It.IsAny<Exception>(),
265+
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
266+
Times.Once);
267+
```
268+
269+
## Unit testing activity functions
270+
271+
Activity functions require no Durable-specific modifications to be tested. The guidance found in the Azure Functions unit testing overview is sufficient for testing these functions.
272+
273+
In this section, the unit test validates the behavior of the `SayHello` Activity function:
274+
275+
```csharp
276+
[Function(nameof(SayHello))]
277+
public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext)
278+
{
279+
return $"Hello {name}!";
280+
}
281+
```
282+
283+
The unit test verifies the format of the output:
284+
285+
```csharp
286+
[Fact]
287+
public void SayHello_ReturnsExpectedGreeting()
288+
{
289+
var functionContextMock = new Mock<FunctionContext>();
290+
291+
const string name = "Tokyo";
292+
293+
var result = HelloCitiesOrchestration.SayHello(name, functionContextMock.Object);
294+
295+
// Verify the activity function SayHello returns the right result
296+
Assert.Equal($"Hello {name}!", result);
297+
}
298+
```
299+
300+
> [!NOTE]
301+
> Currently, loggers created via FunctionContext in activity functions aren't supported for mocking in unit tests.
302+
303+
## Next steps
304+
305+
* Learn more about [xUnit](https://xunit.net/)
306+
* Learn more about [Moq](https://github.com/moq/moq4)
307+
* Learn more about [Azure Functions isolated worker model](../dotnet-isolated-process-guide.md)

articles/azure-functions/durable/durable-functions-unit-testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ ms.topic: conceptual
55
ms.date: 11/03/2019
66
---
77

8-
# Durable Functions unit testing (C#)
8+
# Durable Functions unit testing (C# in-process)
99

1010
Unit testing is an important part of modern software development practices. Unit tests verify business logic behavior and protect from introducing unnoticed breaking changes in the future. Durable Functions can easily grow in complexity so introducing unit tests helps avoid breaking changes. The following sections explain how to unit test the three function types - Orchestration client, orchestrator, and activity functions.
1111

0 commit comments

Comments
 (0)