|
| 1 | +--- |
| 2 | +title: Unit Testing Durable Functions in Python |
| 3 | +description: Learn about unit testing best practices for Durable functions written in Python for Azure Functions. |
| 4 | +author: andystaples |
| 5 | +ms.topic: conceptual |
| 6 | +ms.date: 05/07/2025 |
| 7 | +ms.author: azfuncdf |
| 8 | +--- |
| 9 | + |
| 10 | +# Unit testing Durable Functions in Python |
| 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 entity functions. |
| 13 | + |
| 14 | +> [!NOTE] |
| 15 | +> This guide applies only to Durable Functions apps written in the [Python v2 programming model](../functions-reference-python.md). |
| 16 | +
|
| 17 | +## Prerequisites |
| 18 | + |
| 19 | +The examples in this article require knowledge of the following concepts and frameworks: |
| 20 | + |
| 21 | +* Unit testing |
| 22 | +* Durable Functions |
| 23 | +* Python [unittest](https://docs.python.org/3/library/unittest.html) |
| 24 | +* [unittest.mock](https://docs.python.org/3/library/unittest.mock.html) |
| 25 | + |
| 26 | +## Setting up the test environment |
| 27 | + |
| 28 | +To test Durable Functions, it's crucial to set up a proper test environment. This includes creating a test directory and installing Python's `unittest` module into your Python environment. For more info, see the [Azure Functions Python unit testing overview](../functions-reference-python.md#unit-testing). |
| 29 | + |
| 30 | +## Unit testing trigger functions |
| 31 | + |
| 32 | +Trigger functions, often referred to as _client_ functions, initiate orchestrations and external events. To test these functions: |
| 33 | + |
| 34 | +- Mock the `DurableOrchestrationClient` to simulate orchestration execution and status management. |
| 35 | +- Assign `DurableOrchestrationClient` methods such as `start_new`, `get_status`, or `raise_event` with mock functions that return expected values. |
| 36 | +- Invoke the client function directly with a mocked client and other necessary inputs such as a `req` (HTTP request object) for HTTP trigger client functions. |
| 37 | +- Use assertions and `unittest.mock` tools to verify expected orchestration start behavior, parameters, and HTTP responses. |
| 38 | + |
| 39 | +```python |
| 40 | +import asyncio |
| 41 | +import unittest |
| 42 | +import azure.functions as func |
| 43 | +from unittest.mock import AsyncMock, Mock, patch |
| 44 | + |
| 45 | +from function_app import start_orchestrator |
| 46 | + |
| 47 | +class TestFunction(unittest.TestCase): |
| 48 | + @patch('azure.durable_functions.DurableOrchestrationClient') |
| 49 | + def test_HttpStart(self, client): |
| 50 | + # Get the original method definition as seen in the function_app.py file |
| 51 | + func_call = http_start.build().get_user_function().client_function |
| 52 | + |
| 53 | + req = func.HttpRequest(method='GET', |
| 54 | + body=b'{}', |
| 55 | + url='/api/my_second_function', |
| 56 | + route_params={"functionName": "my_orchestrator"}) |
| 57 | + |
| 58 | + client.start_new = AsyncMock(return_value="instance_id") |
| 59 | + client.create_check_status_response = Mock(return_value="check_status_response") |
| 60 | + |
| 61 | + # Execute the function code |
| 62 | + result = asyncio.run(func_call(req, client)) |
| 63 | + |
| 64 | + client.start_new.assert_called_once_with("my_orchestrator") |
| 65 | + client.create_check_status_response.assert_called_once_with(req, "instance_id") |
| 66 | + self.assertEqual(result, "check_status_response") |
| 67 | +``` |
| 68 | + |
| 69 | +## Unit testing orchestrator functions |
| 70 | + |
| 71 | +Orchestrator functions manage the execution of multiple activity functions. To test an orchestrator: |
| 72 | + |
| 73 | +- Mock the `DurableOrchestrationContext` to control function execution. |
| 74 | +- Replace `DurableOrchestrationContext` methods needed for orchestrator execution like `call_activity` or `create_timer` with mock functions. These functions will typically return objects of type TaskBase with a `result` property. |
| 75 | +- Call the orchestrator recursively, passing the result of the Task generated by the previous yield statement to the next. |
| 76 | +- Verify the orchestrator result using the results returned from the orchestrator and `unittest.mock`. |
| 77 | + |
| 78 | +```python |
| 79 | +import unittest |
| 80 | +from unittest.mock import Mock, patch, call |
| 81 | +from datetime import timedelta |
| 82 | +from azure.durable_functions.testing import orchestrator_generator_wrapper |
| 83 | + |
| 84 | +from function_app import my_orchestrator |
| 85 | + |
| 86 | + |
| 87 | +class TestFunction(unittest.TestCase): |
| 88 | + @patch('azure.durable_functions.DurableOrchestrationContext') |
| 89 | + def test_chaining_orchestrator(self, context): |
| 90 | + # Get the original method definition as seen in the function_app.py file |
| 91 | + func_call = my_orchestrator.build().get_user_function().orchestrator_function |
| 92 | + |
| 93 | + # The mock_activity method is defined above with behavior specific to your app. |
| 94 | + # It returns a TaskBase object with the result expected from the activity call. |
| 95 | + context.call_activity = Mock(side_effect=mock_activity) |
| 96 | + |
| 97 | + # Create a generator using the method and mocked context |
| 98 | + user_orchestrator = func_call(context) |
| 99 | + |
| 100 | + # Use orchestrator_generator_wrapper to get the values from the generator. |
| 101 | + # Processes the orchestrator in a way that is equivalent to the Durable replay logic |
| 102 | + values = [val for val in orchestrator_generator_wrapper(user_orchestrator)] |
| 103 | + |
| 104 | + expected_activity_calls = [call('say_hello', 'Tokyo'), |
| 105 | + call('say_hello', 'Seattle'), |
| 106 | + call('say_hello', 'London')] |
| 107 | + |
| 108 | + self.assertEqual(context.call_activity.call_count, 3) |
| 109 | + self.assertEqual(context.call_activity.call_args_list, expected_activity_calls) |
| 110 | + self.assertEqual(values[3], ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]) |
| 111 | +``` |
| 112 | + |
| 113 | +## Unit testing entity functions |
| 114 | + |
| 115 | +Entity functions manage stateful objects with operations. To test an entity function: |
| 116 | + |
| 117 | +- Mock the `DurableEntityContext` to simulate the entity's internal state and operation inputs. |
| 118 | +- Replace `DurableEntityContext` methods like `get_state`, `set_state`, and `operation_name` with mocks that return controlled values. |
| 119 | +- Invoke the entity function directly with the mocked context. |
| 120 | +- Use assertions to verify state changes and returned values, along with `unittest.mock` utilities. |
| 121 | + |
| 122 | +```python |
| 123 | +import unittest |
| 124 | +from unittest.mock import Mock, patch |
| 125 | + |
| 126 | +from function_app import Counter |
| 127 | + |
| 128 | +class TestEntityFunction(unittest.TestCase): |
| 129 | + @patch('azure.durable_functions.DurableEntityContext') |
| 130 | + def test_entity_add_operation(self, context_mock): |
| 131 | + # Get the original method definition as seen in function_app.py |
| 132 | + func_call = Counter.build().get_user_function().entity_function |
| 133 | + |
| 134 | + # Setup mock context behavior |
| 135 | + state = 0 |
| 136 | + result = None |
| 137 | + |
| 138 | + def set_state(new_state): |
| 139 | + nonlocal state |
| 140 | + state = new_state |
| 141 | + |
| 142 | + def set_result(new_result): |
| 143 | + nonlocal result |
| 144 | + result = new_result |
| 145 | + |
| 146 | + context_mock.get_state = Mock(return_value=state) |
| 147 | + context_mock.set_state = Mock(side_effect=set_state) |
| 148 | + |
| 149 | + context_mock.operation_name = "add" |
| 150 | + context_mock.get_input = Mock(return_value=5) |
| 151 | + |
| 152 | + context_mock.set_result = Mock(side_effect=lambda x: set_result) |
| 153 | + |
| 154 | + # Call the entity function with the mocked context |
| 155 | + func_call(context_mock) |
| 156 | + |
| 157 | + # Verify the state was updated correctly |
| 158 | + context_mock.set_state.assert_called_once_with(5) |
| 159 | + self.assertEqual(state, 5) |
| 160 | + self.assertEqual(result, None) |
| 161 | +``` |
| 162 | + |
| 163 | +## Unit testing activity functions |
| 164 | + |
| 165 | +Activity functions require no Durable-specific modifications to be tested. The guidance found in the [Azure Functions Python unit testing overview](../functions-reference-python.md#unit-testing) is sufficient for testing these functions. |
| 166 | + |
| 167 | +## Related content |
| 168 | + |
| 169 | +- [Learn how to improve throughput performance of Python apps in Azure Functions](../python-scale-performance-reference.md) |
| 170 | +- [Read the Azure Functions Python Developer Guide](../functions-reference-python.md) |
| 171 | +- [Learn about Durable Functions best practices and diagnostic tools](./durable-functions-best-practice-reference.md) |
0 commit comments