PYTEST.md: focused on the core TDD loop (red-green-refactor), simple assertions, and basic project setup. Suitable for learning TDD. PYTEST-ADVANCED.md: adds fixtures, Flask/Quart test patterns, parametrize, and mocking for when the basics are comfortable.
4.3 KiB
Pytest TDD Workflow
Guide development using test-driven development (TDD) with pytest. Always write the test first, watch it fail, then write the minimal code to make it pass.
Keep things simple. Use plain assert statements, simple test functions, and avoid advanced patterns unless the user asks for them. When more advanced techniques (fixtures, parametrize, mocking) would help, briefly explain what they are and suggest the PYTEST-ADVANCED skill.
Instructions
- Never write implementation code before its test exists and fails.
- Follow the red-green-refactor cycle below for each unit of functionality.
- Run tests after every change.
- If the project lacks pytest configuration, set it up first (Section 2).
- Explain each step as you go — the user is learning TDD.
1. The Red-Green-Refactor Cycle
Repeat this cycle for every piece of functionality:
Red — Write a failing test
- Write a test that describes the desired behavior.
- Run the test. Confirm it fails.
- Check that it fails for the right reason (e.g.
AssertionError, notSyntaxError).
Green — Make it pass
- Write the minimum code needed to make the test pass. No more.
- Run the test. Confirm it passes.
Refactor — Clean up
- Improve the code (remove duplication, clarify names) while keeping all tests green.
- Run tests after each change.
Label each phase (Red/Green/Refactor) when presenting work so the workflow is visible.
2. Project Setup
Directory layout
project/
├── src/
│ └── myapp/
│ └── __init__.py
├── tests/
│ └── test_myapp.py
└── pyproject.toml
pyproject.toml
If pytest config does not exist, add it:
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"
Install pytest
uv add --dev pytest
3. Writing the Failing Test (Red)
Naming
- Test files:
test_<module>.py - Test functions:
test_<behavior>— describe what is being tested
# Good — describes behavior
def test_new_user_has_default_role():
...
def test_login_rejects_invalid_password():
...
# Bad — describes implementation
def test_user_init():
...
One test at a time
Write a single test, run it, see it fail, then implement. Do not write a batch of tests upfront.
Assert clearly
Use plain assert — pytest provides detailed failure messages automatically.
def test_new_user_has_default_role():
user = User(name="alice")
assert user.role == "viewer"
Run and confirm failure
pytest tests/test_myapp.py::test_new_user_has_default_role -v
Read the error. Make sure it's the failure you expect before moving on.
4. Making the Test Pass (Green)
Write the minimal implementation to make the test pass.
- Don't add features the test doesn't require.
- Don't handle edge cases that aren't tested yet.
- It's fine to hard-code a return value if only one test exists — the next test will force a real implementation.
Run the full test suite:
pytest -v
All tests must pass before moving on.
5. Refactor
With all tests green, clean up:
- Remove duplication.
- Rename for clarity.
- Simplify logic.
Run tests after every change. If something breaks, undo and try a smaller step.
Don't add new behavior during refactoring. If you spot missing behavior, go back to Red.
6. Testing Exceptions
When a function should raise an error, test for it:
import pytest
def test_divide_by_zero_raises():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
7. Test Quality Checklist
After completing a set of tests, review:
- Every public function has at least one test.
- Tests are isolated — no test depends on another test running first.
- Assertions are specific — test exact values (
assert x == 5, notassert x). - Error paths are tested — not just the happy path.
- Test names describe behavior — reading only test names tells you what the code does.
- No logic in tests — no
if,for, ortry/exceptin test functions. - Tests run fast.
Running with coverage
uv add --dev pytest-cov
pytest --cov=src --cov-report=term-missing -v