Skip to content

Commit 1d78236

Browse files
committed
initial commit
1 parent bbce921 commit 1d78236

File tree

2 files changed

+337
-1
lines changed

2 files changed

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

0 commit comments

Comments
 (0)