microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
.github/instructions/coding-standards/python-tests.instructions.md
238lines · modecode
| 1 | --- |
| 2 | applyTo: '**/*.py' |
| 3 | description: 'Required instructions for Python test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core' |
| 4 | --- |
| 5 | |
| 6 | # Python Test Instructions |
| 7 | |
| 8 | Conventions for Python test code. All conventions from [python-script.instructions.md](python-script.instructions.md) apply. |
| 9 | |
| 10 | ## Test Framework |
| 11 | |
| 12 | Use pytest with BDD-style naming. Structure each test with Arrange/Act/Assert (AAA) sections separated by blank lines and comments. |
| 13 | |
| 14 | ### Mocking Libraries |
| 15 | |
| 16 | | Library | Usage | |
| 17 | |--------------------------------|--------------------------------------------------------| |
| 18 | | pytest-mock (`mocker` fixture) | Preferred for new projects and test migrations | |
| 19 | | monkeypatch | Acceptable for simple attribute/environment patching | |
| 20 | | unittest.mock (direct import) | Existing projects only; migrate to mocker when editing | |
| 21 | |
| 22 | ## When to Use mocker vs monkeypatch |
| 23 | |
| 24 | * `mocker.patch()` — replacing functions, methods, classes, or module attributes with controlled return values or side effects; verifying call counts and arguments. |
| 25 | * `monkeypatch.setattr()` — simple attribute overrides (constants, config values, environment variables) where return tracking is not needed. |
| 26 | * Direct `MagicMock()` import — acceptable for constructing pure test data stubs (mock objects used as constructor arguments, not as spy/assert targets). |
| 27 | |
| 28 | ## Test Naming |
| 29 | |
| 30 | Test method format: `test_given_context_when_action_then_expected` |
| 31 | |
| 32 | ```text |
| 33 | test_given_valid_request_when_process_data_then_returns_parsed_response |
| 34 | test_given_empty_input_when_process_data_then_raises_value_error |
| 35 | test_given_missing_config_when_initialize_then_exits_with_error |
| 36 | ``` |
| 37 | |
| 38 | Prefer one assertion per test. Related assertions validating the same behavior are acceptable. Do not verify logger mocks. |
| 39 | |
| 40 | Use `@pytest.mark.parametrize` for data-driven tests with multiple input/output combinations. |
| 41 | |
| 42 | ## Test Organization |
| 43 | |
| 44 | * File naming mirrors module under test with `test_` prefix (for example, `parser.py` → `test_parser.py`). |
| 45 | * Fixtures in `conftest.py` when shared across multiple test files. |
| 46 | * Class-based grouping optional; use when tests share setup logic. |
| 47 | * Group test methods by behavior, alphabetically within groups. |
| 48 | * Common mock setup in fixtures or class-level setup; specific setup in individual tests. |
| 49 | |
| 50 | ## pytest-mock Patterns |
| 51 | |
| 52 | The `mocker` fixture from pytest-mock replaces direct `unittest.mock` usage. These patterns show each migration. |
| 53 | |
| 54 | ### mocker.patch() replacing @patch decorator |
| 55 | |
| 56 | ```python |
| 57 | # Before — unittest.mock |
| 58 | from unittest.mock import patch |
| 59 | |
| 60 | |
| 61 | @patch("myapp.service.fetch_data") |
| 62 | def test_process_uses_fetched_data(mock_fetch): |
| 63 | mock_fetch.return_value = {"key": "value"} |
| 64 | result = process() |
| 65 | assert result == "value" |
| 66 | |
| 67 | |
| 68 | # After — pytest-mock |
| 69 | def test_process_uses_fetched_data(mocker): |
| 70 | mock_fetch = mocker.patch("myapp.service.fetch_data", return_value={"key": "value"}) |
| 71 | result = process() |
| 72 | assert result == "value" |
| 73 | mock_fetch.assert_called_once() |
| 74 | ``` |
| 75 | |
| 76 | ### mocker.patch() replacing with patch() context manager |
| 77 | |
| 78 | ```python |
| 79 | # Before — unittest.mock |
| 80 | from unittest.mock import patch |
| 81 | |
| 82 | |
| 83 | def test_service_calls_endpoint(): |
| 84 | with patch("myapp.client.post") as mock_post: |
| 85 | mock_post.return_value.status_code = 200 |
| 86 | response = send_request() |
| 87 | assert response.status_code == 200 |
| 88 | |
| 89 | |
| 90 | # After — pytest-mock |
| 91 | def test_service_calls_endpoint(mocker): |
| 92 | mock_post = mocker.patch("myapp.client.post") |
| 93 | mock_post.return_value.status_code = 200 |
| 94 | response = send_request() |
| 95 | assert response.status_code == 200 |
| 96 | ``` |
| 97 | |
| 98 | ### mocker.patch.dict() replacing @patch.dict |
| 99 | |
| 100 | ```python |
| 101 | # Before — unittest.mock |
| 102 | from unittest.mock import patch |
| 103 | |
| 104 | |
| 105 | @patch.dict("os.environ", {"API_KEY": "test-key"}) |
| 106 | def test_config_reads_env(): |
| 107 | config = load_config() |
| 108 | assert config.api_key == "test-key" |
| 109 | |
| 110 | |
| 111 | # After — pytest-mock |
| 112 | def test_config_reads_env(mocker): |
| 113 | mocker.patch.dict("os.environ", {"API_KEY": "test-key"}) |
| 114 | config = load_config() |
| 115 | assert config.api_key == "test-key" |
| 116 | ``` |
| 117 | |
| 118 | ### mocker.patch.object() replacing patch.object() |
| 119 | |
| 120 | ```python |
| 121 | # Before — unittest.mock |
| 122 | from unittest.mock import patch |
| 123 | |
| 124 | from myapp.service import DataService |
| 125 | |
| 126 | |
| 127 | @patch.object(DataService, "connect") |
| 128 | def test_service_connects(mock_connect): |
| 129 | mock_connect.return_value = True |
| 130 | svc = DataService() |
| 131 | assert svc.connect() is True |
| 132 | |
| 133 | |
| 134 | # After — pytest-mock |
| 135 | from myapp.service import DataService |
| 136 | |
| 137 | |
| 138 | def test_service_connects(mocker): |
| 139 | mock_connect = mocker.patch.object(DataService, "connect", return_value=True) |
| 140 | svc = DataService() |
| 141 | assert svc.connect() is True |
| 142 | mock_connect.assert_called_once() |
| 143 | ``` |
| 144 | |
| 145 | ### mocker.MagicMock() and mocker.AsyncMock() for spy targets |
| 146 | |
| 147 | Use `mocker.MagicMock()` and `mocker.AsyncMock()` when constructing mock objects that serve as spy targets for call assertion: |
| 148 | |
| 149 | ```python |
| 150 | def test_handler_delegates_to_processor(mocker): |
| 151 | mock_processor = mocker.MagicMock() |
| 152 | handler = RequestHandler(processor=mock_processor) |
| 153 | handler.handle({"id": 1}) |
| 154 | mock_processor.process.assert_called_once_with({"id": 1}) |
| 155 | |
| 156 | |
| 157 | async def test_async_handler_awaits_processor(mocker): |
| 158 | mock_processor = mocker.AsyncMock() |
| 159 | handler = AsyncRequestHandler(processor=mock_processor) |
| 160 | await handler.handle({"id": 1}) |
| 161 | mock_processor.process.assert_awaited_once_with({"id": 1}) |
| 162 | ``` |
| 163 | |
| 164 | ### Direct MagicMock() import for test data stubs |
| 165 | |
| 166 | Direct `MagicMock()` import stays as-is when constructing pure test data stubs that are not spy/assert targets: |
| 167 | |
| 168 | ```python |
| 169 | from unittest.mock import MagicMock |
| 170 | |
| 171 | |
| 172 | def test_formatter_accepts_any_writer(): |
| 173 | # Arrange |
| 174 | stub_writer = MagicMock() |
| 175 | stub_writer.encoding = "utf-8" |
| 176 | formatter = OutputFormatter(writer=stub_writer) |
| 177 | |
| 178 | # Act |
| 179 | result = formatter.format("hello") |
| 180 | |
| 181 | # Assert |
| 182 | assert result == "hello" |
| 183 | ``` |
| 184 | |
| 185 | ## Complete Example |
| 186 | |
| 187 | A full test class using the mocker fixture with AAA structure: |
| 188 | |
| 189 | ```python |
| 190 | import pytest # noqa: F811 |
| 191 | |
| 192 | from myapp.processor import DataProcessor |
| 193 | from myapp.service import DataService |
| 194 | |
| 195 | |
| 196 | class TestDataProcessor: |
| 197 | @pytest.fixture() |
| 198 | def mock_service(self, mocker): |
| 199 | return mocker.patch.object(DataService, "fetch", return_value={"status": "ok", "value": 42}) |
| 200 | |
| 201 | @pytest.fixture() |
| 202 | def processor(self): |
| 203 | return DataProcessor(service=DataService()) |
| 204 | |
| 205 | def test_given_valid_response_when_process_then_returns_value(self, processor, mock_service): |
| 206 | # Act |
| 207 | result = processor.process() |
| 208 | |
| 209 | # Assert |
| 210 | assert result == 42 |
| 211 | mock_service.assert_called_once() |
| 212 | |
| 213 | def test_given_error_response_when_process_then_raises(self, processor, mocker): |
| 214 | # Arrange |
| 215 | mocker.patch.object(DataService, "fetch", side_effect=ConnectionError("timeout")) |
| 216 | |
| 217 | # Act & Assert |
| 218 | with pytest.raises(ConnectionError, match="timeout"): |
| 219 | processor.process() |
| 220 | |
| 221 | @pytest.mark.parametrize( |
| 222 | ("status", "expected"), |
| 223 | [ |
| 224 | ("ok", 42), |
| 225 | ("pending", 0), |
| 226 | ], |
| 227 | ) |
| 228 | def test_given_status_when_process_then_returns_expected(self, mocker, status, expected): |
| 229 | # Arrange |
| 230 | mocker.patch.object(DataService, "fetch", return_value={"status": status, "value": expected}) |
| 231 | processor = DataProcessor(service=DataService()) |
| 232 | |
| 233 | # Act |
| 234 | result = processor.process() |
| 235 | |
| 236 | # Assert |
| 237 | assert result == expected |
| 238 | ``` |
| 239 | |