Skip to content

Commit 23799b8

Browse files
committed
Merge branch 'main' of https://github.com/MicrosoftDocs/azure-docs-pr into scalablegw
2 parents 6685149 + d33ce01 commit 23799b8

File tree

73 files changed

+460
-35
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+460
-35
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

articles/azure-web-pubsub/socket-io-serverless-tutorial.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Find full code samples that are used in this tutorial:
2020
> [!IMPORTANT]
2121
> Default Mode needs a persistent server, you cannot integration Web PubSub for Socket.IO in default mode with Azure Function.
2222
23+
[!INCLUDE [Connection string security](includes/web-pubsub-connection-string-security.md)]
24+
2325
## Prerequisites
2426

2527
> [!div class="checklist"]

articles/communication-services/concepts/advanced-messaging/whatsapp/pricing.md

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,48 +29,27 @@ The Azure Communication Services Advanced Messaging usage fee is based on the nu
2929

3030
### WhatsApp
3131

32-
When you connect your WhatsApp Business account to Azure, Azure Communication Services becomes the billing entity for your WhatsApp usage. WhatsApp provides these rates and are included in your Azure bill. The information given summarizes the key aspects of WhatsApp pricing. WhatsApp describes their pricing in detail here at [Conversation-Based Pricing](https://developers.facebook.com/docs/whatsapp/pricing).
32+
When you connect your WhatsApp Business account to Azure, Azure Communication Services becomes the billing entity for your WhatsApp usage. WhatsApp provides these rates, and they’re included in your Azure bill. WhatsApp describes their pricing in detail here: [WhatsApp Pricing Documentation](https://developers.facebook.com/docs/whatsapp/pricing).
3333

34-
WhatsApp charges per conversation, not individual message. Conversations are message threads between a business and its customers that last 24 or 72 hours based on the conversation category. Conversations are categorized with one of the following categories:
3534

36-
- **Marketing** conversations include promotions or offers, informational updates, or invitations for customers to respond or take action.
37-
- **Utility** conversations facilitate a specific, agreed-upon request or transaction, or update to a customer about an ongoing transaction. These conversations might include transaction confirmations, transaction updates, and/or post-purchase notifications.
38-
- **Authentication** conversations enable you to authenticate users with one-time passcodes, potentially at multiple steps in the sign in process. Authentication can include account verification, account recovery, and integrity challenges.
39-
- **Service** conversations help you resolve customer inquiries.
35+
**Effective July 1, 2025**, WhatsApp will implement the following changes to their pricing model:
4036

41-
For service conversations, WhatsApp provides 1,000 free conversations each month across all business phone numbers. Marketing, utility and authentication conversations aren't part of the free tier.
37+
- The conversation-based pricing model will be deprecated.
38+
- WhatsApp will charge [per-message for template messages](https://developers.facebook.com/docs/whatsapp/pricing/updates-to-pricing#per-message-pricing) instead of per-conversation.
39+
- Utility template messages sent within an open customer service window [will become free](https://developers.facebook.com/docs/whatsapp/pricing/updates-to-pricing#free-utility-templates-in-the-customer-service-window).
4240

43-
WhatsApp rates vary based on conversation category and country/region rate. Rates vary between \$0.003 and \$0.1597 depending on the category and country/region. WhatsApp provides a detailed explanation of their pricing, including the current rate card here: [Conversation-Based Pricing](https://developers.facebook.com/docs/whatsapp/pricing).
41+
See [Pricing Updates on the WhatsApp Business Platform](https://developers.facebook.com/docs/whatsapp/pricing/updates-to-pricing/) for additional details.
4442

4543
## Pricing example: Contoso sends appointment reminders to their WhatsApp customers
4644

4745
Contoso provides a virtual visit solution for its patients. Contoso is scheduling the visit and sends WhatsApp invites to all patients reminding them about their upcoming visit. WhatsApp classifies appointment reminders as **Utility Conversations**. In this case, each WhatsApp conversation is a single message.
48-
4946
Contoso sends appointment reminders to 2,000 patients in North America each month and the pricing would be:
5047

5148
**Advanced Messaging usage for messages:**
5249

53-
2,000 WhatsApp Conversations = 2,000 messages x \$0.005/message = \$10 USD
54-
55-
**WhatsApp Fees (rates subject to change):**
56-
57-
2,000 WhatsApp Conversations \* \$0.015/utility conversation = \$30 USD
58-
59-
To get the latest WhatsApp rates, refer to WhatsApp’s pricing documentation: [Conversation-Based Pricing](https://developers.facebook.com/docs/whatsapp/pricing).
60-
61-
## Pricing example: A WhatsApp user reaches out to a business for support
62-
63-
Contoso is a business that provides a contact center for customers to seek product information and support. All these cases are closed within 24 hours and have an average of 20 messages each. Each case equals one WhatsApp Conversation. WhatsApp classifies contact center conversations as *Service Conversations*.
64-
65-
Contoso manages 2,000 cases in North America each month and the pricing would be:
66-
67-
**Advanced Messaging usage for conversation:**
68-
69-
2,000 WhatsApp Conversations \* 20 messages/conversation x \$0.005/message = \$200 USD
70-
71-
**WhatsApp Fees (rates subject to change):** 1,000 WhatsApp free conversations/month + 1,000 WhatsApp conversations \* \$0.0088/service conversation = \$8.80 USD
50+
2,000 messages × \$0.005/message = \$10.00 USD + WhatsApp Fee
7251

73-
To get the latest WhatsApp rates, refer to WhatsApp’s pricing documentation: [Conversation-Based Pricing](https://developers.facebook.com/docs/whatsapp/pricing).
52+
Please see WhatsApp pricing in detail here: [WhatsApp Pricing Documentation](https://developers.facebook.com/docs/whatsapp/pricing).
7453

7554
## Next steps
7655

0 commit comments

Comments
 (0)