microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
a3acef32dec8d8ac8051793df3686007a92266cd

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

.github/instructions/coding-standards/python-tests.instructions.md

238lines · modecode

1---
2applyTo: '**/*.py'
3description: '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
8Conventions for Python test code. All conventions from [python-script.instructions.md](python-script.instructions.md) apply.
9
10## Test Framework
11
12Use 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
30Test method format: `test_given_context_when_action_then_expected`
31
32```text
33test_given_valid_request_when_process_data_then_returns_parsed_response
34test_given_empty_input_when_process_data_then_raises_value_error
35test_given_missing_config_when_initialize_then_exits_with_error
36```
37
38Prefer one assertion per test. Related assertions validating the same behavior are acceptable. Do not verify logger mocks.
39
40Use `@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
52The `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
58from unittest.mock import patch
59
60
61@patch("myapp.service.fetch_data")
62def 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
69def 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
80from unittest.mock import patch
81
82
83def 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
91def 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
102from unittest.mock import patch
103
104
105@patch.dict("os.environ", {"API_KEY": "test-key"})
106def test_config_reads_env():
107 config = load_config()
108 assert config.api_key == "test-key"
109
110
111# After — pytest-mock
112def 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
122from unittest.mock import patch
123
124from myapp.service import DataService
125
126
127@patch.object(DataService, "connect")
128def 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
135from myapp.service import DataService
136
137
138def 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
147Use `mocker.MagicMock()` and `mocker.AsyncMock()` when constructing mock objects that serve as spy targets for call assertion:
148
149```python
150def 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
157async 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
166Direct `MagicMock()` import stays as-is when constructing pure test data stubs that are not spy/assert targets:
167
168```python
169from unittest.mock import MagicMock
170
171
172def 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
187A full test class using the mocker fixture with AAA structure:
188
189```python
190import pytest # noqa: F811
191
192from myapp.processor import DataProcessor
193from myapp.service import DataService
194
195
196class 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