22title : Azure Durable Functions unit testing
33description : Learn how to unit test Durable Functions.
44ms.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
4343In 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
4775The 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.
72101durableClientMock
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
120145After 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
126210Orchestrator functions are even more interesting for unit testing since they usually have a lot more business logic.
127211
128212In 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
132247The unit test code starts with creating a mock:
133248
@@ -160,19 +275,75 @@ Assert.Equal("Hello London!", result[2]);
160275
161276After 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
167310Activity functions are unit tested in the same way as nondurable functions.
168311
169312In 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
173323And 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