Skip to content

Commit e597525

Browse files
committed
update code to correct code for mocking
Signed-off-by: Hannah Hunter <[email protected]>
1 parent 95d6787 commit e597525

1 file changed

Lines changed: 196 additions & 25 deletions

File tree

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

Lines changed: 196 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: Azure Durable Functions unit testing
33
description: Learn how to unit test Durable Functions.
44
ms.topic: conceptual
5-
ms.date: 11/03/2019
5+
ms.date: 02/13/2026
66
---
77

88
# Durable Functions unit testing (C# in-process)
@@ -42,7 +42,35 @@ These interfaces can be used with the various trigger and bindings supported by
4242

4343
In this section, the unit test validates the logic of the following HTTP trigger function for starting new orchestrations.
4444

45-
[!code-csharp[Main](~/samples-durable-functions/samples/precompiled/HttpStart.cs)]
45+
```csharp
46+
using System.Net.Http;
47+
using System.Threading.Tasks;
48+
using Microsoft.Azure.WebJobs;
49+
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
50+
using Microsoft.Azure.WebJobs.Extensions.Http;
51+
using Microsoft.Extensions.Logging;
52+
53+
namespace VSSample
54+
{
55+
public static class HttpStart
56+
{
57+
[FunctionName("HttpStart")]
58+
public static async Task<HttpResponseMessage> Run(
59+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
60+
[DurableClient] IDurableClient starter,
61+
string functionName,
62+
ILogger log)
63+
{
64+
object eventData = await req.Content.ReadAsAsync<object>();
65+
string instanceId = await starter.StartNewAsync(functionName, eventData);
66+
67+
log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
68+
69+
return starter.CreateCheckStatusResponse(req, instanceId);
70+
}
71+
}
72+
}
73+
```
4674

4775
The unit test task verifies the value of the `Retry-After` header provided in the response payload. So the unit test mocks some of `IDurableClient` methods to ensure predictable behavior.
4876

@@ -65,26 +93,26 @@ durableClientMock.
6593
ReturnsAsync(instanceId);
6694
```
6795

68-
Next `CreateCheckStatusResponse` is mocked to always return an empty HTTP 200 response.
96+
Next, the test needs to handle `CreateCheckStatusResponse`. Since `CreateCheckStatusResponse` is an extension method, it can't be mocked directly with Moq. Instead, mock the underlying `CreateHttpManagementPayload` method, which is an instance method on `IDurableClient`:
6997

7098
```csharp
71-
// Mock CreateCheckStatusResponse method
99+
// CreateCheckStatusResponse is an extension method and cannot be mocked directly.
100+
// Mock CreateHttpManagementPayload, which is the underlying instance method.
72101
durableClientMock
73-
// Notice that even though the HttpStart function does not call IDurableClient.CreateCheckStatusResponse()
74-
// with the optional parameter returnInternalServerErrorOnFailure, moq requires the method to be set up
75-
// with each of the optional parameters provided. Simply use It.IsAny<> for each optional parameter
76-
.Setup(x => x.CreateCheckStatusResponse(It.IsAny<HttpRequestMessage>(), instanceId, returnInternalServerErrorOnFailure: It.IsAny<bool>()))
77-
.Returns(new HttpResponseMessage
102+
.Setup(x => x.CreateHttpManagementPayload(instanceId))
103+
.Returns(new HttpManagementPayload
78104
{
79-
StatusCode = HttpStatusCode.OK,
80-
Content = new StringContent(string.Empty),
81-
Headers =
82-
{
83-
RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10))
84-
}
105+
Id = instanceId,
106+
StatusQueryGetUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}",
107+
SendEventPostUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/raiseEvent/{{eventName}}",
108+
TerminatePostUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/terminate",
109+
PurgeHistoryDeleteUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}"
85110
});
86111
```
87112

113+
> [!NOTE]
114+
> `CreateCheckStatusResponse` is an extension method that internally calls `CreateHttpManagementPayload`. Extension methods are static and cannot be mocked using standard mocking frameworks like Moq. By mocking `CreateHttpManagementPayload`, you control the data used by the extension method.
115+
88116
`ILogger` is also mocked:
89117

90118
```csharp
@@ -110,24 +138,111 @@ var result = await HttpStart.Run(
110138
The last step is to compare the output with the expected value:
111139

112140
```csharp
113-
// Validate that output is not null
114-
Assert.NotNull(result.Headers.RetryAfter);
115-
116-
// Validate output's Retry-After header value
117-
Assert.Equal(TimeSpan.FromSeconds(10), result.Headers.RetryAfter.Delta);
141+
// Validate the response status code
142+
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
118143
```
119144

120145
After you combine all these steps, the unit test has the following code:
121146

122-
[!code-csharp[Main](~/samples-durable-functions/samples/VSSample.Tests/HttpStartTests.cs)]
147+
```csharp
148+
using System;
149+
using System.Net;
150+
using System.Net.Http;
151+
using System.Text;
152+
using System.Threading.Tasks;
153+
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
154+
using Microsoft.Extensions.Logging;
155+
using Moq;
156+
using Xunit;
157+
158+
namespace VSSample.Tests
159+
{
160+
public class HttpStartTests
161+
{
162+
[Fact]
163+
public async Task HttpStart_returns_management_payload()
164+
{
165+
// Arrange
166+
string instanceId = "7E467BDB-213F-407A-B86A-1954053D3C24";
167+
string functionName = "E1_HelloSequence";
168+
169+
var durableClientMock = new Mock<IDurableClient>();
170+
171+
durableClientMock
172+
.Setup(x => x.StartNewAsync(functionName, It.IsAny<object>()))
173+
.ReturnsAsync(instanceId);
174+
175+
// CreateCheckStatusResponse is an extension method and cannot be mocked directly.
176+
// Mock CreateHttpManagementPayload, which is the underlying instance method.
177+
durableClientMock
178+
.Setup(x => x.CreateHttpManagementPayload(instanceId))
179+
.Returns(new HttpManagementPayload
180+
{
181+
Id = instanceId,
182+
StatusQueryGetUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}",
183+
SendEventPostUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/raiseEvent/{{eventName}}",
184+
TerminatePostUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/terminate",
185+
PurgeHistoryDeleteUri = $"http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}"
186+
});
187+
188+
var loggerMock = new Mock<ILogger>();
189+
190+
// Act
191+
var result = await HttpStart.Run(
192+
new HttpRequestMessage
193+
{
194+
Content = new StringContent("{}", Encoding.UTF8, "application/json"),
195+
RequestUri = new Uri("http://localhost:7071/orchestrators/E1_HelloSequence"),
196+
},
197+
durableClientMock.Object,
198+
functionName,
199+
loggerMock.Object);
200+
201+
// Assert
202+
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
203+
}
204+
}
205+
}
206+
```
123207

124208
## Unit testing orchestrator functions
125209

126210
Orchestrator functions are even more interesting for unit testing since they usually have a lot more business logic.
127211

128212
In this section, the unit tests validate the output of the `E1_HelloSequence` Orchestrator function:
129213

130-
[!code-csharp[Main](~/samples-durable-functions/samples/precompiled/HelloSequence.cs)]
214+
```csharp
215+
using System.Collections.Generic;
216+
using System.Threading.Tasks;
217+
using Microsoft.Azure.WebJobs;
218+
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
219+
220+
namespace VSSample
221+
{
222+
public static class HelloSequence
223+
{
224+
[FunctionName("E1_HelloSequence")]
225+
public static async Task<List<string>> Run(
226+
[OrchestrationTrigger] IDurableOrchestrationContext context)
227+
{
228+
var outputs = new List<string>();
229+
230+
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Tokyo"));
231+
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "Seattle"));
232+
outputs.Add(await context.CallActivityAsync<string>("E1_SayHello", "London"));
233+
234+
return outputs;
235+
}
236+
237+
[FunctionName("E1_SayHello")]
238+
public static string SayHello([ActivityTrigger] IDurableActivityContext context)
239+
{
240+
string name = context.GetInput<string>();
241+
return $"Hello {name}!";
242+
}
243+
}
244+
}
245+
```
131246

132247
The unit test code starts with creating a mock:
133248

@@ -160,19 +275,75 @@ Assert.Equal("Hello London!", result[2]);
160275

161276
After you combine the previous steps, the unit test has the following code:
162277

163-
[!code-csharp[Main](~/samples-durable-functions/samples/VSSample.Tests/HelloSequenceOrchestratorTests.cs)]
278+
```csharp
279+
using System.Collections.Generic;
280+
using System.Threading.Tasks;
281+
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
282+
using Moq;
283+
using Xunit;
284+
285+
namespace VSSample.Tests
286+
{
287+
public class HelloSequenceOrchestratorTests
288+
{
289+
[Fact]
290+
public async Task Run_returns_multiple_greetings()
291+
{
292+
var durableOrchestrationContextMock = new Mock<IDurableOrchestrationContext>();
293+
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Tokyo")).ReturnsAsync("Hello Tokyo!");
294+
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "Seattle")).ReturnsAsync("Hello Seattle!");
295+
durableOrchestrationContextMock.Setup(x => x.CallActivityAsync<string>("E1_SayHello", "London")).ReturnsAsync("Hello London!");
296+
297+
var result = await HelloSequence.Run(durableOrchestrationContextMock.Object);
298+
299+
Assert.Equal(3, result.Count);
300+
Assert.Equal("Hello Tokyo!", result[0]);
301+
Assert.Equal("Hello Seattle!", result[1]);
302+
Assert.Equal("Hello London!", result[2]);
303+
}
304+
}
305+
}
306+
```
164307

165308
## Unit testing activity functions
166309

167310
Activity functions are unit tested in the same way as nondurable functions.
168311

169312
In this section the unit test validates the behavior of the `E1_SayHello` Activity function:
170313

171-
[!code-csharp[Main](~/samples-durable-functions/samples/precompiled/HelloSequence.cs)]
314+
```csharp
315+
[FunctionName("E1_SayHello")]
316+
public static string SayHello([ActivityTrigger] IDurableActivityContext context)
317+
{
318+
string name = context.GetInput<string>();
319+
return $"Hello {name}!";
320+
}
321+
```
172322

173323
And the unit tests verify the format of the output. These unit tests either use the parameter types directly or mock `IDurableActivityContext` class:
174324

175-
[!code-csharp[Main](~/samples-durable-functions/samples/VSSample.Tests/HelloSequenceActivityTests.cs)]
325+
```csharp
326+
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
327+
using Moq;
328+
using Xunit;
329+
330+
namespace VSSample.Tests
331+
{
332+
public class HelloSequenceActivityTests
333+
{
334+
[Fact]
335+
public void SayHello_returns_greeting()
336+
{
337+
var durableActivityContextMock = new Mock<IDurableActivityContext>();
338+
durableActivityContextMock.Setup(x => x.GetInput<string>()).Returns("World");
339+
340+
var result = HelloSequence.SayHello(durableActivityContextMock.Object);
341+
342+
Assert.Equal("Hello World!", result);
343+
}
344+
}
345+
}
346+
```
176347

177348
## Next steps
178349

0 commit comments

Comments
 (0)