split pytest skill into beginner and advanced versions
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.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
# 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
|
||||
1. Write a test that describes the desired behavior.
|
||||
2. Run the test. **Confirm it fails.**
|
||||
3. Check that it fails for the *right reason* (e.g. `AssertionError`, not `SyntaxError`).
|
||||
|
||||
### Green — Make it pass
|
||||
4. Write the **minimum** code needed to make the test pass. No more.
|
||||
5. Run the test. **Confirm it passes.**
|
||||
|
||||
### Refactor — Clean up
|
||||
6. Improve the code (remove duplication, clarify names) while keeping all tests green.
|
||||
7. 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:
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-v --tb=short"
|
||||
```
|
||||
|
||||
### Install pytest
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```python
|
||||
# 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.
|
||||
|
||||
```python
|
||||
def test_new_user_has_default_role():
|
||||
user = User(name="alice")
|
||||
assert user.role == "viewer"
|
||||
```
|
||||
|
||||
### Run and confirm failure
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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`, not `assert 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`, or `try/except` in test functions.
|
||||
- [ ] **Tests run fast.**
|
||||
|
||||
### Running with coverage
|
||||
|
||||
```bash
|
||||
uv add --dev pytest-cov
|
||||
pytest --cov=src --cov-report=term-missing -v
|
||||
```
|
||||
Reference in New Issue
Block a user