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:
Mark Eaton
2026-02-23 16:54:13 -05:00
parent 6fd6147bb3
commit 275425e630
2 changed files with 189 additions and 168 deletions
@@ -1,161 +1,18 @@
# Pytest TDD Workflow # Pytest TDD Workflow — Advanced
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. Follow the red-green-refactor cycle for every piece of functionality. This skill extends the core PYTEST skill with advanced testing techniques. Use when the basic red-green-refactor cycle is comfortable and you need fixtures, parametrize, mocking, or Flask/Quart test patterns.
**All rules from the core PYTEST skill still apply** — always follow red-green-refactor, always write the test first.
## Instructions ## Instructions
- **Never write implementation code before its test exists and fails.** - Follow the red-green-refactor cycle for every piece of functionality.
- Follow the numbered workflow below for each unit of functionality. - Use the techniques below when they genuinely simplify or improve tests.
- Run tests after every change — both after writing the failing test and after writing the implementation. - Run tests after every change.
- If the project lacks pytest configuration or test infrastructure, set it up first (Section 2).
- Use Flask/Quart testing patterns when working on web applications (Section 7).
--- ---
## 1. The Red-Green-Refactor Cycle ## 1. Fixtures
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.** If it passes, the test is not testing new behavior — revise it.
3. Verify it fails for the *right reason* (e.g. `AssertionError` or `AttributeError`, not `ImportError` or `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 implementation (remove duplication, clarify names, extract functions) while keeping all tests green.
7. Run tests after each refactoring change.
**Show each step explicitly.** When presenting work, label which phase you are in (Red/Green/Refactor) so the workflow is visible.
---
## 2. Project Setup
Before writing tests, ensure the project has proper test infrastructure.
### Directory layout
```
project/
├── src/ # or the package name directly
│ └── myapp/
│ └── __init__.py
├── tests/
│ ├── conftest.py # shared fixtures
│ ├── test_models.py
│ └── test_routes.py
└── pyproject.toml
```
### pyproject.toml — pytest configuration
If pytest config does not exist, add it:
```toml
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"
```
### conftest.py
Create `tests/conftest.py` for shared fixtures. Keep it organized — if it grows large, split fixtures into multiple `conftest.py` files in subdirectories.
### Dependencies
Ensure pytest is installed. Use `uv` for dependency management:
```bash
uv add --dev pytest
```
---
## 3. Writing the Failing Test (Red)
### File naming
- Test files: `test_<module>.py` (e.g. `test_models.py`, `test_auth.py`)
- Test functions: `test_<behavior>` — describe what is being tested, not how
```python
# Good — describes behavior
def test_new_user_has_default_role():
...
def test_login_rejects_invalid_password():
...
# Bad — describes implementation
def test_user_init():
...
def test_check_password_returns_false():
...
```
### Write 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
Each test should have a single, clear assertion (or a small group of closely related assertions). Use plain `assert` statements — pytest's introspection provides detailed failure messages.
```python
def test_new_user_has_default_role():
user = User(name="alice")
assert user.role == "viewer"
```
### Run and confirm failure
```bash
pytest tests/test_models.py::test_new_user_has_default_role -v
```
Read the failure output. Confirm the error is what you expect (e.g. `NameError: name 'User' is not defined` or `AssertionError`). If the failure is something else (syntax error, import misconfiguration), fix the test before proceeding.
---
## 4. Making the Test Pass (Green)
Write the **minimal** implementation to make the failing test pass.
- Do not add features the test does not require.
- Do not handle edge cases that are not yet tested.
- Hard-coding a return value is acceptable if only one test exists — the next test will force a real implementation.
Run the full test suite, not just the new test:
```bash
pytest -v
```
All tests must pass before moving on.
---
## 5. Refactor
With all tests green, improve the code:
- Remove duplication between implementation and tests.
- Rename variables and functions for clarity.
- Extract helper functions or methods.
- Simplify logic.
**Run the full test suite after every refactoring change.** If a test breaks, undo the last change and try a smaller step.
Do not add new behavior during refactoring. If you notice missing behavior, go back to Red and write a test for it first.
---
## 6. Fixtures
Use fixtures for setup code that multiple tests share. Use fixtures for setup code that multiple tests share.
@@ -206,7 +63,7 @@ Prefer `function` scope. Only widen scope when setup is genuinely expensive and
--- ---
## 7. Flask/Quart Test Patterns ## 2. Flask/Quart Test Patterns
### App factory fixture ### App factory fixture
@@ -220,7 +77,6 @@ def app():
app = create_app({ app = create_app({
"TESTING": True, "TESTING": True,
"SECRET_KEY": "test-secret-key", "SECRET_KEY": "test-secret-key",
# override database URI for tests, etc.
}) })
yield app yield app
@@ -307,7 +163,7 @@ async def test_homepage(client):
--- ---
## 8. Parametrize ## 3. Parametrize
Use `@pytest.mark.parametrize` to test multiple inputs without duplicating test functions. Use `@pytest.mark.parametrize` to test multiple inputs without duplicating test functions.
@@ -351,7 +207,7 @@ def test_format_currency_rejects_negative(amount):
--- ---
## 9. Mocking ## 4. Mocking
### When to mock ### When to mock
@@ -393,21 +249,9 @@ def test_registration_sends_welcome_email(mock_send):
) )
``` ```
### Testing exceptions
```python
def test_divide_by_zero_raises():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
def test_invalid_input_message():
with pytest.raises(ValueError, match="must be positive"):
process(-1)
```
--- ---
## 10. Test Quality Checklist ## 5. Test Quality Checklist
After completing a set of tests, review against this checklist: After completing a set of tests, review against this checklist:
+177
View File
@@ -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
```