chiron 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/CLAUDE.md +30 -1
- data/docs/development_journal.md +84 -1
- data/lib/chiron/templates/python/commands/quality/python-testing.md +578 -0
- data/lib/chiron/templates/python/commands/workflows/debug-python.md +222 -0
- data/lib/chiron/templates/python/commands/workflows/flask-development.md +667 -0
- data/lib/chiron/templates/python/commands/workflows/python-refactor.md +336 -0
- data/lib/chiron/templates/shared/commands/context/branch-context.md +176 -0
- data/lib/chiron/templates/shared/commands/context/catchup.md +6 -1
- data/lib/chiron/templates/shared/commands/context/quickstart.md +8 -3
- data/lib/chiron/templates/shared/commands/workflows/branch-management.md +256 -0
- data/lib/chiron/templates/shared/development_journal.md.erb +9 -3
- data/lib/chiron/version.rb +1 -1
- metadata +7 -1
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
# Python Testing Patterns
|
|
2
|
+
|
|
3
|
+
Comprehensive guide for testing Python applications using pytest and related tools.
|
|
4
|
+
|
|
5
|
+
## Testing Philosophy
|
|
6
|
+
|
|
7
|
+
### Test Pyramid Structure
|
|
8
|
+
- **Unit Tests (70%)**: Fast, focused tests for individual functions/methods
|
|
9
|
+
- **Integration Tests (20%)**: Test interactions between components
|
|
10
|
+
- **End-to-End Tests (10%)**: Full application workflow tests
|
|
11
|
+
|
|
12
|
+
### TDD Approach
|
|
13
|
+
1. Write a failing test
|
|
14
|
+
2. Write minimal code to make it pass
|
|
15
|
+
3. Refactor while keeping tests green
|
|
16
|
+
|
|
17
|
+
## pytest Fundamentals
|
|
18
|
+
|
|
19
|
+
### Basic Test Structure
|
|
20
|
+
```python
|
|
21
|
+
# test_example.py
|
|
22
|
+
import pytest
|
|
23
|
+
from myapp import Calculator
|
|
24
|
+
|
|
25
|
+
def test_addition():
|
|
26
|
+
calc = Calculator()
|
|
27
|
+
result = calc.add(2, 3)
|
|
28
|
+
assert result == 5
|
|
29
|
+
|
|
30
|
+
def test_division_by_zero():
|
|
31
|
+
calc = Calculator()
|
|
32
|
+
with pytest.raises(ZeroDivisionError):
|
|
33
|
+
calc.divide(10, 0)
|
|
34
|
+
|
|
35
|
+
def test_with_multiple_assertions():
|
|
36
|
+
calc = Calculator()
|
|
37
|
+
assert calc.add(1, 1) == 2
|
|
38
|
+
assert calc.subtract(5, 3) == 2
|
|
39
|
+
assert calc.multiply(3, 4) == 12
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Test Discovery and Execution
|
|
43
|
+
```bash
|
|
44
|
+
# Run all tests
|
|
45
|
+
pytest
|
|
46
|
+
|
|
47
|
+
# Run specific test file
|
|
48
|
+
pytest test_calculator.py
|
|
49
|
+
|
|
50
|
+
# Run specific test
|
|
51
|
+
pytest test_calculator.py::test_addition
|
|
52
|
+
|
|
53
|
+
# Run with verbose output
|
|
54
|
+
pytest -v
|
|
55
|
+
|
|
56
|
+
# Run with coverage
|
|
57
|
+
pytest --cov=myapp
|
|
58
|
+
|
|
59
|
+
# Run parallel tests
|
|
60
|
+
pytest -n auto # Requires pytest-xdist
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Fixtures for Test Setup
|
|
64
|
+
|
|
65
|
+
### Basic Fixtures
|
|
66
|
+
```python
|
|
67
|
+
import pytest
|
|
68
|
+
from myapp import Database, User
|
|
69
|
+
|
|
70
|
+
@pytest.fixture
|
|
71
|
+
def db_connection():
|
|
72
|
+
"""Provide a database connection for tests."""
|
|
73
|
+
db = Database()
|
|
74
|
+
db.connect()
|
|
75
|
+
yield db
|
|
76
|
+
db.disconnect()
|
|
77
|
+
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def sample_user():
|
|
80
|
+
"""Provide a sample user for tests."""
|
|
81
|
+
return User(
|
|
82
|
+
username="testuser",
|
|
83
|
+
email="test@example.com",
|
|
84
|
+
age=25
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def test_user_creation(db_connection, sample_user):
|
|
88
|
+
user_id = db_connection.create_user(sample_user)
|
|
89
|
+
assert user_id is not None
|
|
90
|
+
|
|
91
|
+
retrieved_user = db_connection.get_user(user_id)
|
|
92
|
+
assert retrieved_user.username == "testuser"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Fixture Scopes
|
|
96
|
+
```python
|
|
97
|
+
@pytest.fixture(scope="session")
|
|
98
|
+
def database_setup():
|
|
99
|
+
"""Run once per test session."""
|
|
100
|
+
# Setup expensive resources
|
|
101
|
+
yield
|
|
102
|
+
# Cleanup
|
|
103
|
+
|
|
104
|
+
@pytest.fixture(scope="module")
|
|
105
|
+
def api_client():
|
|
106
|
+
"""Run once per test module."""
|
|
107
|
+
client = APIClient()
|
|
108
|
+
client.authenticate()
|
|
109
|
+
yield client
|
|
110
|
+
client.logout()
|
|
111
|
+
|
|
112
|
+
@pytest.fixture(scope="function") # Default scope
|
|
113
|
+
def clean_environment():
|
|
114
|
+
"""Run for each test function."""
|
|
115
|
+
setup_test_environment()
|
|
116
|
+
yield
|
|
117
|
+
cleanup_test_environment()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Parameterized Tests
|
|
121
|
+
```python
|
|
122
|
+
import pytest
|
|
123
|
+
|
|
124
|
+
@pytest.mark.parametrize("input,expected", [
|
|
125
|
+
(2, 4),
|
|
126
|
+
(3, 9),
|
|
127
|
+
(4, 16),
|
|
128
|
+
(5, 25),
|
|
129
|
+
])
|
|
130
|
+
def test_square_function(input, expected):
|
|
131
|
+
from myapp import square
|
|
132
|
+
assert square(input) == expected
|
|
133
|
+
|
|
134
|
+
@pytest.mark.parametrize("username,email,valid", [
|
|
135
|
+
("user1", "user1@example.com", True),
|
|
136
|
+
("", "user2@example.com", False), # Empty username
|
|
137
|
+
("user3", "invalid-email", False), # Invalid email
|
|
138
|
+
("user4", "", False), # Empty email
|
|
139
|
+
])
|
|
140
|
+
def test_user_validation(username, email, valid):
|
|
141
|
+
from myapp import User
|
|
142
|
+
user = User(username=username, email=email)
|
|
143
|
+
assert user.is_valid() == valid
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Mocking and Patching
|
|
147
|
+
|
|
148
|
+
### Using unittest.mock
|
|
149
|
+
```python
|
|
150
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
151
|
+
import pytest
|
|
152
|
+
|
|
153
|
+
# Mock objects
|
|
154
|
+
def test_with_mock():
|
|
155
|
+
mock_service = Mock()
|
|
156
|
+
mock_service.get_data.return_value = {"status": "success"}
|
|
157
|
+
mock_service.process_data.side_effect = ValueError("Invalid data")
|
|
158
|
+
|
|
159
|
+
# Test code that uses mock_service
|
|
160
|
+
assert mock_service.get_data()["status"] == "success"
|
|
161
|
+
|
|
162
|
+
with pytest.raises(ValueError):
|
|
163
|
+
mock_service.process_data("bad_data")
|
|
164
|
+
|
|
165
|
+
# Patching external dependencies
|
|
166
|
+
@patch('myapp.external_api.requests.get')
|
|
167
|
+
def test_api_call(mock_get):
|
|
168
|
+
from myapp.external_api import fetch_user_data
|
|
169
|
+
|
|
170
|
+
# Setup mock response
|
|
171
|
+
mock_response = Mock()
|
|
172
|
+
mock_response.json.return_value = {"id": 1, "name": "John"}
|
|
173
|
+
mock_response.status_code = 200
|
|
174
|
+
mock_get.return_value = mock_response
|
|
175
|
+
|
|
176
|
+
# Test
|
|
177
|
+
result = fetch_user_data(user_id=1)
|
|
178
|
+
assert result["name"] == "John"
|
|
179
|
+
mock_get.assert_called_once_with("https://api.example.com/users/1")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### pytest-mock Plugin
|
|
183
|
+
```python
|
|
184
|
+
# pip install pytest-mock
|
|
185
|
+
def test_with_mocker(mocker):
|
|
186
|
+
# Mock using mocker fixture
|
|
187
|
+
mock_service = mocker.Mock()
|
|
188
|
+
mock_service.get_user.return_value = {"id": 1, "name": "John"}
|
|
189
|
+
|
|
190
|
+
# Patch class method
|
|
191
|
+
mocker.patch('myapp.UserService.get_user', return_value={"id": 1})
|
|
192
|
+
|
|
193
|
+
# Patch with side effect
|
|
194
|
+
mocker.patch('myapp.send_email', side_effect=Exception("Email failed"))
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Testing Async Code
|
|
198
|
+
|
|
199
|
+
### Testing Async Functions
|
|
200
|
+
```python
|
|
201
|
+
import pytest
|
|
202
|
+
import asyncio
|
|
203
|
+
|
|
204
|
+
# Mark async tests
|
|
205
|
+
@pytest.mark.asyncio
|
|
206
|
+
async def test_async_function():
|
|
207
|
+
from myapp import fetch_data_async
|
|
208
|
+
|
|
209
|
+
result = await fetch_data_async()
|
|
210
|
+
assert result is not None
|
|
211
|
+
|
|
212
|
+
# Async fixtures
|
|
213
|
+
@pytest.fixture
|
|
214
|
+
async def async_client():
|
|
215
|
+
client = AsyncClient()
|
|
216
|
+
await client.connect()
|
|
217
|
+
yield client
|
|
218
|
+
await client.disconnect()
|
|
219
|
+
|
|
220
|
+
@pytest.mark.asyncio
|
|
221
|
+
async def test_with_async_fixture(async_client):
|
|
222
|
+
result = await async_client.get_data()
|
|
223
|
+
assert result["status"] == "ok"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Testing with aioresponses
|
|
227
|
+
```python
|
|
228
|
+
# pip install aioresponses
|
|
229
|
+
import aiohttp
|
|
230
|
+
from aioresponses import aioresponses
|
|
231
|
+
|
|
232
|
+
@pytest.mark.asyncio
|
|
233
|
+
async def test_async_http_call():
|
|
234
|
+
with aioresponses() as mock:
|
|
235
|
+
mock.get('http://api.example.com/data', payload={"key": "value"})
|
|
236
|
+
|
|
237
|
+
async with aiohttp.ClientSession() as session:
|
|
238
|
+
result = await fetch_from_api(session)
|
|
239
|
+
assert result["key"] == "value"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Database Testing
|
|
243
|
+
|
|
244
|
+
### SQLAlchemy Testing
|
|
245
|
+
```python
|
|
246
|
+
import pytest
|
|
247
|
+
from sqlalchemy import create_engine
|
|
248
|
+
from sqlalchemy.orm import sessionmaker
|
|
249
|
+
from myapp.models import Base, User
|
|
250
|
+
|
|
251
|
+
@pytest.fixture(scope="session")
|
|
252
|
+
def engine():
|
|
253
|
+
return create_engine("sqlite:///:memory:")
|
|
254
|
+
|
|
255
|
+
@pytest.fixture(scope="session")
|
|
256
|
+
def tables(engine):
|
|
257
|
+
Base.metadata.create_all(engine)
|
|
258
|
+
yield
|
|
259
|
+
Base.metadata.drop_all(engine)
|
|
260
|
+
|
|
261
|
+
@pytest.fixture
|
|
262
|
+
def db_session(engine, tables):
|
|
263
|
+
Session = sessionmaker(bind=engine)
|
|
264
|
+
session = Session()
|
|
265
|
+
yield session
|
|
266
|
+
session.rollback()
|
|
267
|
+
session.close()
|
|
268
|
+
|
|
269
|
+
def test_user_creation(db_session):
|
|
270
|
+
user = User(username="testuser", email="test@example.com")
|
|
271
|
+
db_session.add(user)
|
|
272
|
+
db_session.commit()
|
|
273
|
+
|
|
274
|
+
assert user.id is not None
|
|
275
|
+
|
|
276
|
+
retrieved_user = db_session.query(User).filter_by(username="testuser").first()
|
|
277
|
+
assert retrieved_user.email == "test@example.com"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Django Testing
|
|
281
|
+
```python
|
|
282
|
+
from django.test import TestCase, Client
|
|
283
|
+
from django.contrib.auth.models import User
|
|
284
|
+
from myapp.models import Article
|
|
285
|
+
|
|
286
|
+
class ArticleTestCase(TestCase):
|
|
287
|
+
def setUp(self):
|
|
288
|
+
self.user = User.objects.create_user(
|
|
289
|
+
username='testuser',
|
|
290
|
+
password='testpass'
|
|
291
|
+
)
|
|
292
|
+
self.article = Article.objects.create(
|
|
293
|
+
title="Test Article",
|
|
294
|
+
content="Test content",
|
|
295
|
+
author=self.user
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def test_article_creation(self):
|
|
299
|
+
self.assertEqual(self.article.title, "Test Article")
|
|
300
|
+
self.assertEqual(self.article.author, self.user)
|
|
301
|
+
|
|
302
|
+
def test_article_str_representation(self):
|
|
303
|
+
self.assertEqual(str(self.article), "Test Article")
|
|
304
|
+
|
|
305
|
+
class ArticleViewTestCase(TestCase):
|
|
306
|
+
def setUp(self):
|
|
307
|
+
self.client = Client()
|
|
308
|
+
self.user = User.objects.create_user(
|
|
309
|
+
username='testuser',
|
|
310
|
+
password='testpass'
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def test_article_list_view(self):
|
|
314
|
+
response = self.client.get('/articles/')
|
|
315
|
+
self.assertEqual(response.status_code, 200)
|
|
316
|
+
|
|
317
|
+
def test_create_article_requires_auth(self):
|
|
318
|
+
response = self.client.post('/articles/create/', {
|
|
319
|
+
'title': 'New Article',
|
|
320
|
+
'content': 'Content'
|
|
321
|
+
})
|
|
322
|
+
self.assertEqual(response.status_code, 302) # Redirect to login
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## API Testing
|
|
326
|
+
|
|
327
|
+
### FastAPI Testing
|
|
328
|
+
```python
|
|
329
|
+
from fastapi.testclient import TestClient
|
|
330
|
+
from myapp.main import app
|
|
331
|
+
|
|
332
|
+
client = TestClient(app)
|
|
333
|
+
|
|
334
|
+
def test_read_main():
|
|
335
|
+
response = client.get("/")
|
|
336
|
+
assert response.status_code == 200
|
|
337
|
+
assert response.json() == {"message": "Hello World"}
|
|
338
|
+
|
|
339
|
+
def test_create_item():
|
|
340
|
+
response = client.post("/items/", json={
|
|
341
|
+
"name": "Test Item",
|
|
342
|
+
"description": "Test Description",
|
|
343
|
+
"price": 10.5
|
|
344
|
+
})
|
|
345
|
+
assert response.status_code == 201
|
|
346
|
+
data = response.json()
|
|
347
|
+
assert data["name"] == "Test Item"
|
|
348
|
+
assert "id" in data
|
|
349
|
+
|
|
350
|
+
def test_authentication():
|
|
351
|
+
# Test without auth
|
|
352
|
+
response = client.get("/protected")
|
|
353
|
+
assert response.status_code == 401
|
|
354
|
+
|
|
355
|
+
# Test with auth
|
|
356
|
+
headers = {"Authorization": "Bearer valid_token"}
|
|
357
|
+
response = client.get("/protected", headers=headers)
|
|
358
|
+
assert response.status_code == 200
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Testing with requests-mock
|
|
362
|
+
```python
|
|
363
|
+
import requests
|
|
364
|
+
import requests_mock
|
|
365
|
+
|
|
366
|
+
def test_external_api_call():
|
|
367
|
+
with requests_mock.Mocker() as mock:
|
|
368
|
+
mock.get('https://api.example.com/users/1', json={
|
|
369
|
+
'id': 1,
|
|
370
|
+
'name': 'John Doe'
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
response = requests.get('https://api.example.com/users/1')
|
|
374
|
+
assert response.json()['name'] == 'John Doe'
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Test Organization
|
|
378
|
+
|
|
379
|
+
### Conftest.py for Shared Fixtures
|
|
380
|
+
```python
|
|
381
|
+
# conftest.py
|
|
382
|
+
import pytest
|
|
383
|
+
from myapp import create_app
|
|
384
|
+
|
|
385
|
+
@pytest.fixture(scope="session")
|
|
386
|
+
def app():
|
|
387
|
+
"""Create application for testing."""
|
|
388
|
+
app = create_app(testing=True)
|
|
389
|
+
return app
|
|
390
|
+
|
|
391
|
+
@pytest.fixture
|
|
392
|
+
def client(app):
|
|
393
|
+
"""Create test client."""
|
|
394
|
+
return app.test_client()
|
|
395
|
+
|
|
396
|
+
@pytest.fixture
|
|
397
|
+
def runner(app):
|
|
398
|
+
"""Create CLI runner."""
|
|
399
|
+
return app.test_cli_runner()
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Test Markers
|
|
403
|
+
```python
|
|
404
|
+
import pytest
|
|
405
|
+
|
|
406
|
+
@pytest.mark.slow
|
|
407
|
+
def test_slow_operation():
|
|
408
|
+
# Test that takes a long time
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
@pytest.mark.integration
|
|
412
|
+
def test_database_integration():
|
|
413
|
+
# Integration test
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
@pytest.mark.unit
|
|
417
|
+
def test_pure_function():
|
|
418
|
+
# Unit test
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
# Run specific marked tests
|
|
422
|
+
# pytest -m slow
|
|
423
|
+
# pytest -m "not slow"
|
|
424
|
+
# pytest -m "unit and not slow"
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Coverage and Quality
|
|
428
|
+
|
|
429
|
+
### Coverage Configuration
|
|
430
|
+
```ini
|
|
431
|
+
# .coveragerc
|
|
432
|
+
[run]
|
|
433
|
+
source = myapp
|
|
434
|
+
omit =
|
|
435
|
+
*/venv/*
|
|
436
|
+
*/tests/*
|
|
437
|
+
*/migrations/*
|
|
438
|
+
manage.py
|
|
439
|
+
setup.py
|
|
440
|
+
|
|
441
|
+
[report]
|
|
442
|
+
exclude_lines =
|
|
443
|
+
pragma: no cover
|
|
444
|
+
def __repr__
|
|
445
|
+
raise AssertionError
|
|
446
|
+
raise NotImplementedError
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Property-Based Testing with Hypothesis
|
|
450
|
+
```python
|
|
451
|
+
from hypothesis import given, strategies as st
|
|
452
|
+
|
|
453
|
+
@given(st.integers(), st.integers())
|
|
454
|
+
def test_addition_commutative(a, b):
|
|
455
|
+
assert add(a, b) == add(b, a)
|
|
456
|
+
|
|
457
|
+
@given(st.lists(st.integers(), min_size=1))
|
|
458
|
+
def test_sort_idempotent(numbers):
|
|
459
|
+
sorted_once = sorted(numbers)
|
|
460
|
+
sorted_twice = sorted(sorted_once)
|
|
461
|
+
assert sorted_once == sorted_twice
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Testing Best Practices
|
|
465
|
+
|
|
466
|
+
### Test Naming
|
|
467
|
+
```python
|
|
468
|
+
# Good test names describe the scenario
|
|
469
|
+
def test_should_return_empty_list_when_no_users_exist():
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
def test_should_raise_validation_error_when_email_is_invalid():
|
|
473
|
+
pass
|
|
474
|
+
|
|
475
|
+
def test_should_create_user_with_hashed_password():
|
|
476
|
+
pass
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Test Structure (Arrange-Act-Assert)
|
|
480
|
+
```python
|
|
481
|
+
def test_user_registration():
|
|
482
|
+
# Arrange
|
|
483
|
+
user_data = {
|
|
484
|
+
"username": "newuser",
|
|
485
|
+
"email": "newuser@example.com",
|
|
486
|
+
"password": "securepassword"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
# Act
|
|
490
|
+
result = register_user(user_data)
|
|
491
|
+
|
|
492
|
+
# Assert
|
|
493
|
+
assert result.success is True
|
|
494
|
+
assert result.user.username == "newuser"
|
|
495
|
+
assert result.user.password != "securepassword" # Should be hashed
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Test Data Builders
|
|
499
|
+
```python
|
|
500
|
+
class UserBuilder:
|
|
501
|
+
def __init__(self):
|
|
502
|
+
self.username = "defaultuser"
|
|
503
|
+
self.email = "default@example.com"
|
|
504
|
+
self.age = 25
|
|
505
|
+
|
|
506
|
+
def with_username(self, username):
|
|
507
|
+
self.username = username
|
|
508
|
+
return self
|
|
509
|
+
|
|
510
|
+
def with_email(self, email):
|
|
511
|
+
self.email = email
|
|
512
|
+
return self
|
|
513
|
+
|
|
514
|
+
def build(self):
|
|
515
|
+
return User(
|
|
516
|
+
username=self.username,
|
|
517
|
+
email=self.email,
|
|
518
|
+
age=self.age
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# Usage
|
|
522
|
+
def test_user_validation():
|
|
523
|
+
user = (UserBuilder()
|
|
524
|
+
.with_username("testuser")
|
|
525
|
+
.with_email("test@example.com")
|
|
526
|
+
.build())
|
|
527
|
+
|
|
528
|
+
assert user.is_valid() is True
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
## Continuous Integration
|
|
532
|
+
|
|
533
|
+
### GitHub Actions Example
|
|
534
|
+
```yaml
|
|
535
|
+
# .github/workflows/test.yml
|
|
536
|
+
name: Tests
|
|
537
|
+
|
|
538
|
+
on: [push, pull_request]
|
|
539
|
+
|
|
540
|
+
jobs:
|
|
541
|
+
test:
|
|
542
|
+
runs-on: ubuntu-latest
|
|
543
|
+
strategy:
|
|
544
|
+
matrix:
|
|
545
|
+
python-version: [3.8, 3.9, "3.10", "3.11"]
|
|
546
|
+
|
|
547
|
+
steps:
|
|
548
|
+
- uses: actions/checkout@v3
|
|
549
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
550
|
+
uses: actions/setup-python@v4
|
|
551
|
+
with:
|
|
552
|
+
python-version: ${{ matrix.python-version }}
|
|
553
|
+
|
|
554
|
+
- name: Install dependencies
|
|
555
|
+
run: |
|
|
556
|
+
python -m pip install --upgrade pip
|
|
557
|
+
pip install -r requirements-dev.txt
|
|
558
|
+
|
|
559
|
+
- name: Run tests
|
|
560
|
+
run: |
|
|
561
|
+
pytest --cov=myapp --cov-report=xml
|
|
562
|
+
|
|
563
|
+
- name: Upload coverage
|
|
564
|
+
uses: codecov/codecov-action@v3
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## Testing Checklist
|
|
568
|
+
|
|
569
|
+
- [ ] Tests are fast and focused
|
|
570
|
+
- [ ] Each test has a single responsibility
|
|
571
|
+
- [ ] Test names clearly describe the scenario
|
|
572
|
+
- [ ] Tests are independent and can run in any order
|
|
573
|
+
- [ ] Mock external dependencies
|
|
574
|
+
- [ ] Test both happy path and edge cases
|
|
575
|
+
- [ ] Aim for high coverage but focus on behavior
|
|
576
|
+
- [ ] Use fixtures to reduce duplication
|
|
577
|
+
- [ ] Tests are part of CI/CD pipeline
|
|
578
|
+
- [ ] Flaky tests are investigated and fixed
|