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.
@@ -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