| name | python-testing |
| description | Use when writing tests, setting up pytest, implementing TDD, creating fixtures, or mocking dependencies. Do NOT use for general patterns (use python-patterns) or style config (use python-code-style). |
| paths | **/*.py, **/pyproject.toml, **/pytest.ini, **/conftest.py |
Python Testing Patterns
ํ๋จ ๊ธฐ์ค๊ณผ ๊ท์น ์ค์ฌ. pytest API๊ฐ ์๋, ์ฌ๋ฐ๋ฅธ ํ
์คํธ ์ ๋ต ์ ํ์ ์๋ด.
Quick Start
CRITICAL Rules
- Test behavior, not implementation -- ๋ด๋ถ ๊ตฌํ์ด ์๋ ์
์ถ๋ ฅ/๋ถ์ํจ๊ณผ ๊ฒ์ฆ
- One assertion concept per test -- ํ๋์ ํ
์คํธ๊ฐ ํ๋์ ํ์ ๊ฒ์ฆ
- No test interdependence -- ํ
์คํธ ์์ ๋ฌด๊ดํ๊ฒ ๋
๋ฆฝ ์คํ
- AAA pattern -- Arrange-Act-Assert ๊ตฌ์กฐ ์์
- Mock at boundaries only -- ์ธ๋ถ ์์คํ
(DB, API, filesystem)๋ง mock, ๋ด๋ถ ๋ก์ง์ ์ค์ ์คํ
- NEVER
@patch where you use, not where you define -- patch("myapp.service.requests.get") NOT patch("requests.get")
- ALWAYS
autospec=True on mocks -- API ๋ถ์ผ์น ์กฐ๊ธฐ ๋ฐ๊ฒฌ
- ALWAYS 80%+ coverage, critical paths 100% --
pytest --cov --cov-fail-under=80
- PREFER
pytest.raises(match=...) -- ์์ธ ํ์
+ ๋ฉ์์ง ํจ๊ป ๊ฒ์ฆ
- NEVER test third-party code -- ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋์ํ๋์ง ํ
์คํธํ์ง ๋ง๋ผ
TDD Cycle
1. RED -- Write failing test for desired behavior
2. GREEN -- Write MINIMAL code to pass (no extras)
3. REFACTOR -- Clean up with tests green
4. REPEAT
Rule: GREEN์์ "๋ฏธ๋์ ํ์ํ " ์ฝ๋ ๊ธ์ง. ํ
์คํธ๊ฐ ์๊ตฌํ๋ ๊ฒ๋ง.
Test Strategy Decision
What are you testing?
+-- Pure logic (no dependencies) --> Unit test (no fixtures, no mocking)
+-- Service with dependencies --> Unit test + mock dependencies
+-- API endpoint --> Integration test (TestClient/httpx)
+-- Database operations --> Integration test + test DB (or Testcontainers)
+-- Full user workflow --> E2E test (sparingly)
+-- Data transformation --> Parametrized test
+-- Serialization/format --> Property-based test (Hypothesis) or Snapshot test
Test Pyramid
| Layer | Ratio | Speed | Dependencies |
|---|
| Unit | 70% | ms | None |
| Integration | 20% | seconds | DB, API |
| E2E | 10% | minutes | Full stack |
Fixture Scope Decision
How expensive is setup?
+-- Cheap (in-memory object) --> function (default, safest)
+-- Medium (DB connection) --> module or session
+-- Expensive (Docker container) --> session
+-- Need isolation between tests? --> function (ALWAYS)
+-- Shared read-only data? --> module or session
Key Fixture Patterns
@pytest.fixture
def db_session():
session = create_session()
yield session
session.rollback()
session.close()
@pytest.fixture(params=["sqlite", "postgres"])
def db(request):
return create_db(request.param)
@pytest.fixture(autouse=True)
def reset_config():
Config.reset()
yield
Config.cleanup()
Rules:
session scope fixture์ mutable state ๊ธ์ง
autouse ์ต์ํ์ผ๋ก -- ์์์ ์์กด์ฑ์ ๋๋ฒ๊น
์ด๋ ต๊ฒ ๋ง๋ฆ
conftest.py๋ ํด๋น ๋๋ ํ ๋ฆฌ ํ์์์๋ง ์ ํจ
Mock Decision
Should you mock it?
+-- External HTTP API --> YES (unreliable, slow)
+-- Database --> DEPENDS
| +-- Unit test? --> YES (mock repository)
| +-- Integration test? --> NO (use test DB)
+-- File system --> YES (use tmp_path fixture instead)
+-- Time/randomness --> YES (freezegun, deterministic seed)
+-- Internal class/function --> NO (test the real thing)
+-- Configuration --> DEPENDS (fixture > mock)
Mock Patterns
@patch("myapp.service.payment_client.charge", autospec=True)
def test_process_payment(mock_charge):
mock_charge.return_value = PaymentResult(success=True)
result = process_order(order)
mock_charge.assert_called_once_with(order.amount)
def test_with_mocker(mocker):
mock_api = mocker.patch("myapp.service.api.fetch", autospec=True)
mock_api.return_value = {"status": "ok"}
result = process()
mock_api.assert_called_once()
def test_with_spy(mocker):
spy = mocker.spy(myapp.utils, "validate")
process(data)
spy.assert_called_once_with(data)
Mock Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|
| Mock everything | ํ
์คํธ๊ฐ ๊ตฌํ์ ์ข
์ | ๊ฒฝ๊ณ๋ง mock |
patch("requests.get") | ์ ์ ์์น mockํ๋ฉด ๋ค๋ฅธ ๋ชจ๋์์ ์ ์กํ | ์ฌ์ฉ ์์น patch |
autospec=False (default) | ์กด์ฌํ์ง ์๋ ๋ฉ์๋ ํธ์ถํด๋ ํต๊ณผ | autospec=True |
mock.return_value = Mock() | ํ์
๋ถ์ผ์น ์จ๊น | ์ค์ ๊ฐ์ฒด๋ dataclass ๋ฐํ |
| Mock in integration test | ์ค์ ๋์ ๊ฒ์ฆ ์ ๋จ | ์ค์ ์์กด์ฑ ์ฌ์ฉ |
Parametrization
When to Parametrize
Same logic, different inputs? --> @pytest.mark.parametrize
Same test, different backends? --> @pytest.fixture(params=...)
Edge cases + happy path? --> parametrize with ids
@pytest.mark.parametrize("input,expected", [
("valid@email.com", True),
("invalid", False),
("@no-domain.com", False),
], ids=["valid-email", "missing-at", "missing-domain"])
def test_email_validation(input, expected):
assert is_valid_email(input) is expected
Rule: 5๊ฐ ์ด์ ํ๋ผ๋ฏธํฐ๋ฉด ids ํ์ -- ์คํจ ์ ์ด๋ค ์ผ์ด์ค์ธ์ง ์ฆ์ ํ์
Async Testing
@pytest.mark.asyncio
async def test_async_fetch():
result = await fetch_data("endpoint")
assert result.status == "ok"
@pytest.mark.asyncio
async def test_async_mock(mocker):
mock = mocker.patch("myapp.client.fetch", autospec=True)
mock.return_value = {"data": []}
result = await process()
mock.assert_awaited_once()
Test Organization
tests/
+-- conftest.py # Shared fixtures (DB, client, auth)
+-- unit/
| +-- conftest.py # Unit-specific fixtures
| +-- test_services.py
| +-- test_models.py
+-- integration/
| +-- conftest.py # DB session, test containers
| +-- test_api.py
| +-- test_repository.py
+-- e2e/
+-- test_user_flow.py
Naming Convention
def test_create_user_valid_input_returns_user():
def test_create_user_duplicate_email_raises_conflict():
def test_get_user_not_found_returns_none():
pytest Configuration
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = [
"--strict-markers",
"--cov=src",
"--cov-report=term-missing",
"--cov-fail-under=80",
"-x",
"--tb=short",
]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: integration tests",
]
[tool.coverage.run]
source = ["src"]
omit = ["tests/*", "*/migrations/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"@abstractmethod",
]
CLI Quick Reference
| Command | Use When |
|---|
pytest -x | ์ฒซ ์คํจ์์ ๋ฉ์ถค |
pytest --lf | ๋ง์ง๋ง ์คํจํ ํ
์คํธ๋ง |
pytest -k "user" | ์ด๋ฆ ํจํด ๋งค์นญ |
pytest -m "not slow" | ๋๋ฆฐ ํ
์คํธ ์ ์ธ |
pytest -n auto | ๋ณ๋ ฌ ์คํ (pytest-xdist) |
pytest --pdb | ์คํจ ์ ๋๋ฒ๊ฑฐ ์ง์
|
Coverage Gap Analysis Template
๊ธฐ์กด ์ฝ๋์ ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๋ถ์ํ ๋ ์ฌ์ฉํ๋ ์ถ๋ ฅ ํ์.
## Test Coverage Analysis
### Current Coverage
- Tests: [X] tests covering [Y] functions/modules
- Line coverage: [Z]%
- Coverage gaps: [list of uncovered areas]
### Recommended Tests
1. **[test_name]** โ [What it verifies, why it matters]
2. **[test_name]** โ [What it verifies, why it matters]
### Priority
- Critical: [Tests that catch data loss or security issues]
- High: [Tests for core business logic]
- Medium: [Tests for edge cases and error handling]
- Low: [Tests for utility functions and formatting]
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|
| ๊ตฌํ ํ
์คํธ | ๋ฆฌํฉํ ๋งํ๋ฉด ํ
์คํธ ๊นจ์ง | ์
์ถ๋ ฅ/ํ์๋ง ๊ฒ์ฆ |
| ํ
์คํธ ๊ฐ ๊ณต์ ์ํ | ์์ ์์กด์ฑ, ๊ฐํ์ ์คํจ | fixture๋ก ๊ฒฉ๋ฆฌ |
assert True / assert result | ์คํจํด๋ ์์ธ ๋ชจ๋ฆ | ๊ตฌ์ฒด์ ๊ฐ ๋น๊ต |
try/except in test | ์์ธ ์ผํด | pytest.raises ์ฌ์ฉ |
| ํ
์คํธ์ ์กฐ๊ฑด๋ฌธ | ํ
์คํธ ์์ฒด๊ฐ ๋ฒ๊ทธ ๊ฐ๋ฅ | ๊ฐ ๊ฒฝ๋ก๋ฅผ ๋ณ๋ ํ
์คํธ |
print() for debugging | ๋
ธ์ด์ฆ | pytest -s ๋๋ --pdb |
| ๋๋ฆฐ ํ
์คํธ ๋ฏธ๋ถ๋ฆฌ | CI ํผ๋๋ฐฑ ์ง์ฐ | @pytest.mark.slow |
Gotchas
- โ
patch('module.Class') (์ ์ ์์น) โ patch('consumer_module.Class') (์ฌ์ฉ ์์น)
- โ
autospec=True ๋๋ฝ โ ์๋ชป๋ ์๊ทธ๋์ฒ ๊ฐ์ง ๋ถ๊ฐ
- โ fixture scope ๋ถ์ผ์น (session fixture๊ฐ function fixture ์์กด) โ scope ๊ณ์ธต ์ค์
- โ
assert mock.called โ mock.assert_called_once_with(expected) ์ฌ์ฉ (๋ ๋ช
์์ )
Troubleshooting
| Symptom | Cause | Solution |
|---|
fixture not found | conftest.py ์์น ์๋ชป | ํด๋น ๋๋ ํ ๋ฆฌ์ conftest.py ํ์ธ |
| ๊ฐํ์ ํ
์คํธ ์คํจ | ํ
์คํธ ๊ฐ ์ํ ๊ณต์ | pytest-randomly๋ก ์์ ๋ฌด์์ํ ํ ์์ธ ์ถ์ |
patch ์ ๋จนํ | ์ ์ ์์น๊ฐ ์๋ ์ฌ์ฉ ์์น patch ํ์ | import ๊ฒฝ๋ก ํ์ธ |
| async test ๋ฌด์๋จ | pytest-asyncio mode ์ค์ ๋๋ฝ | mode = "auto" in pyproject.toml |
| coverage ๋ฎ์ | ํ
์คํธ ๊ฒฝ๋ก ๋ถ์ผ์น | [tool.coverage.run] source ํ์ธ |
Cross-References
| Topic | Skill |
|---|
| Python ํจํด, ํ์
ํํธ, ๋์์ฑ | python-patterns |
| Ruff, mypy, formatting, naming | python-code-style |
| Hypothesis, factory_boy, snapshot, plugins | references/advanced-testing.md |
| TDD methodology (general) | test-driven-development superpowers |
| pytest ์คํจ triage, flaky ํ
์คํธ, git bisect | debugging |
References