Skip to content

Commit 4e45d7b

Browse files
committed
Initial PR for Durable Functions unit testing docs
1 parent b149543 commit 4e45d7b

File tree

4 files changed

+182
-1
lines changed

4 files changed

+182
-1
lines changed

articles/azure-functions/.openpublishing.redirection.azure-functions.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@
190190
"redirect_url": "/azure/azure-functions/durable/durable-functions-unit-testing",
191191
"redirect_document_id": false
192192
},
193+
{
194+
"source_path_from_root": "/articles/azure-functions/durable-functions-unit-testing-python.md",
195+
"redirect_url": "/azure/azure-functions/durable/durable-functions-unit-testing-python",
196+
"redirect_document_id": false
197+
},
193198
{
194199
"source_path_from_root": "/articles/azure-functions/durable-functions-versioning.md",
195200
"redirect_url": "/azure/azure-functions/durable/durable-functions-versioning",

articles/azure-functions/durable/TOC.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,10 @@
155155
href: durable-functions-create-portal.md
156156
- name: Manage connections
157157
href: ../manage-connections.md?toc=/azure/azure-functions/durable/toc.json
158-
- name: Unit testing
158+
- name: Unit testing (C#)
159159
href: durable-functions-unit-testing.md
160+
- name: Unit testing (Python)
161+
href: durable-functions-unit-testing-python.md
160162
- name: Create as WebJobs
161163
href: durable-functions-webjobs-sdk.md
162164
- name: Durable entities in .NET
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Unit Testing Durable Functions in Python
2+
3+
## Introduction
4+
Unit testing Durable Functions is essential to ensure the correctness of individual components without relying on an actual Azure environment. By writing effective unit tests, developers can catch errors early, improve code maintainability, and increase confidence in their implementations.
5+
6+
This guide provides an overview of unit testing Durable Functions in Python, covering the key components: starter functions, orchestrators, activity functions, and entity functions. It includes best practices and sample test cases to help you write robust and maintainable tests for your Durable Functions.
7+
8+
## Prerequisites
9+
The examples in this article require knowledge of the following concepts and frameworks:
10+
11+
* Unit testing
12+
* Durable Functions
13+
* Python [unittest](https://docs.python.org/3/library/unittest.html)
14+
* [unittest.mock](https://docs.python.org/3/library/unittest.mock.html)
15+
16+
## Setting Up the Test Environment
17+
To test Durable Functions, it's crucial to set up a proper test environment. This includes creating a test directory and installing unittest into your Python environment. For more info, see the Azure Functions Python unit testing overview [here](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python?tabs=get-started%2Casgi%2Capplication-level&pivots=python-mode-decorators#unit-testing).
18+
19+
## Testing Durable Clients
20+
Durable Client functions initiate orchestrations and external events. To test a client function:
21+
22+
- Mock the `DurableOrchestrationClient` to simulate orchestration execution and status management.
23+
- Replace `DurableOrchestrationClient` methods such as `start_new`, `get_status`, or `raise_event` with mock functions that return expected values.
24+
- Invoke the client function directly with a mocked client as well as other necessary inputs such as a `req` (HTTP request object) for HTTP trigger client functions.
25+
- Use assertions and `unittest.mock` tools to verify expected orchestration start behavior, parameters, and HTTP responses.
26+
27+
**Sample Code:**
28+
```python
29+
import asyncio
30+
import unittest
31+
import azure.functions as func
32+
from unittest.mock import AsyncMock, Mock, patch
33+
34+
from function_app import start_orchestrator
35+
36+
class TestFunction(unittest.TestCase):
37+
@patch('azure.durable_functions.DurableOrchestrationClient')
38+
def test_chaining_orchestrator(self, client):
39+
# Get the original method definition as seen in the function_app.py file
40+
# func_call = chaining_orchestrator.build().get_user_function_unmodified()
41+
func_call = start_orchestrator.build().get_user_function().client_function
42+
43+
req = func.HttpRequest(method='GET',
44+
body=None,
45+
url='/api/my_second_function',
46+
params={"orchestrator_name": "startOrchestrator"})
47+
48+
client.start_new = AsyncMock(return_value="instance_id")
49+
client.create_check_status_response = Mock(return_value="check_status_response")
50+
51+
# Create a generator using the method and mocked context
52+
result = asyncio.run(func_call(req, client))
53+
54+
client.start_new.assert_called_once_with("startOrchestrator")
55+
client.create_check_status_response.assert_called_once_with(req, "instance_id")
56+
self.assertEqual(result, "check_status_response")
57+
```
58+
59+
## Testing Durable Orchestrators
60+
Durable Orchestrators manage the execution of multiple activity functions. To test an orchestrator:
61+
62+
- Mock the `DurableOrchestrationContext` to control function execution.
63+
- Replace `DurableOrchestrationContext` methods needed for orchestrator execution like `call_activity` or `create_timer` with mock functions. These functions should have pre-determined behavior and should typically return Task objects with a `result` property.
64+
- Call the orchestrator recursively, passing the result of the Task generated by the previous yield statement to the next.
65+
- Use the results returned from the orchestrator, as well as `unittest.mock` methods to verify the orchestrator result.
66+
67+
**Sample Code:**
68+
```python
69+
import unittest
70+
from unittest.mock import Mock, patch, call
71+
from datetime import timedelta
72+
73+
class TestFunction(unittest.TestCase):
74+
@patch('azure.durable_functions.DurableOrchestrationContext')
75+
def test_chaining_orchestrator(self, context):
76+
# Get the original method definition as seen in the function_app.py file
77+
func_call = chaining_orchestrator.build().get_user_function().orchestrator_function
78+
79+
context.call_activity = Mock(side_effect=mock_activity)
80+
context.create_timer = Mock(return_value=MockTask())
81+
# Create a generator using the method and mocked context
82+
user_orchestrator = func_call(context)
83+
84+
# Use a method defined above to get the values from the generator. Quick unwrap for easy access
85+
values = [val for val in orchestrator_generator_wrapper(user_orchestrator)]
86+
87+
expected_activity_calls = [call('say_hello', 'Tokyo'),
88+
call('say_hello', 'Seattle'),
89+
call('say_hello', 'London')]
90+
91+
self.assertEqual(context.call_activity.call_count, 3)
92+
self.assertEqual(context.call_activity.call_args_list, expected_activity_calls)
93+
context.create_timer.assert_called_once_with(context.current_utc_datetime + timedelta(seconds=5))
94+
self.assertEqual(values[4], ["Hello Tokyo!", "Hello Seattle!", "Hello London!"])
95+
```
96+
97+
## Testing Durable Entities
98+
Durable Entity functions manage stateful objects with operations. To test an entity function:
99+
100+
- Mock the `DurableEntityContext` to simulate the entity's internal state and operation inputs.
101+
- Replace `DurableEntityContext` methods like `get_state`, `set_state`, and `operation_name` with mocks that return controlled values.
102+
- Invoke the entity function directly with the mocked context.
103+
- Use assertions to verify state changes and returned values, along with `unittest.mock` utilities.
104+
105+
**Sample Code:**
106+
```python
107+
import unittest
108+
from unittest.mock import Mock, patch
109+
110+
from function_app import Counter
111+
112+
class TestEntityFunction(unittest.TestCase):
113+
@patch('azure.durable_functions.DurableEntityContext')
114+
def test_entity_add_operation(self, context_mock):
115+
# Get the original method definition as seen in function_app.py
116+
func_call = Counter.build().get_user_function().entity_function
117+
118+
# Setup mock context behavior
119+
state = 0
120+
result = None
121+
122+
def set_state(new_state):
123+
nonlocal state
124+
state = new_state
125+
126+
def set_result(new_result):
127+
nonlocal result
128+
result = new_result
129+
130+
context_mock.get_state = Mock(return_value=state)
131+
context_mock.set_state = Mock(side_effect=set_state)
132+
133+
context_mock.operation_name = "add"
134+
context_mock.get_input = Mock(return_value=5)
135+
136+
context_mock.set_result = Mock(side_effect=lambda x: set_result)
137+
138+
# Call the entity function with the mocked context
139+
func_call(context_mock)
140+
141+
# Verify the state was updated correctly
142+
context_mock.set_state.assert_called_once_with(5)
143+
self.assertEqual(state, 5)
144+
self.assertEqual(result, None)
145+
```
146+
147+
## Testing Activity Functions
148+
Activity functions require no Durable-specific modifications to be tested. The guidance found in the Azure Functions Python unit testing overview [here](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python?tabs=get-started%2Casgi%2Capplication-level&pivots=python-mode-decorators#unit-testing) is sufficient for testing these functions.
149+
150+
## Summary
151+
Testing Durable Functions effectively requires simulating their unique runtime behaviors while keeping tests isolated and deterministic. Here are some best practices to keep in mind:
152+
153+
- **Mock the right context objects**: Use `DurableOrchestrationContext`, `DurableOrchestrationClient`, and `DurableEntityContext` mocks to simulate platform behavior.
154+
- **Replace key methods**: Mock critical methods like `call_activity`, `start_new`, `get_state`, etc., to return controlled values for validation.
155+
- **Isolate logic**: Keep orchestration, client, and entity logic testable independently to ensure clear separation of concerns.
156+
- **Validate behavior, not implementation**: Focus on validating the outcome and side effects, rather than internal steps.
157+
- **Use generator wrappers**: For orchestrators, properly walk through the generator steps to simulate orchestration flow.
158+
- **Test failure paths**: Include tests for error conditions, timeouts, and unexpected input to ensure robust function behavior.
159+
160+
By applying these practices, you can build a comprehensive test suite for your Durable Functions that ensures reliability and simplifies long-term maintenance.
161+
162+
---
163+
164+
## Related content
165+
166+
For deeper insights into Durable Functions in Python, explore these resources:
167+
168+
- [Durable Functions GitHub Samples](https://github.com/Azure/azure-functions-durable-python)
169+
- [Azure Functions Python Developer Guide](https://learn.microsoft.com/azure/azure-functions/functions-reference-python)
170+
- [Azure Durable Functions Documentation](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview)

articles/azure-functions/durable/quickstart-python-vscode.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ After you verify that the function runs correctly on your local computer, it's t
379379

380380
[!INCLUDE [functions-publish-project-vscode](../../../includes/functions-publish-project-vscode.md)]
381381

382+
## Unit testing
383+
384+
For more information on unit testing functions in Python, see our [Python unit testing guide](durable-functions-unit-testing-python.md)
385+
382386
## Test your function in Azure
383387

384388
::: zone pivot="python-mode-configuration"

0 commit comments

Comments
 (0)