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:
@@ -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
|
||||
|
||||
- **Never write implementation code before its test exists and fails.**
|
||||
- Follow the numbered workflow below for each unit of functionality.
|
||||
- Run tests after every change — both after writing the failing test and after writing the implementation.
|
||||
- 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).
|
||||
- Follow the red-green-refactor cycle for every piece of functionality.
|
||||
- Use the techniques below when they genuinely simplify or improve tests.
|
||||
- Run tests after every change.
|
||||
|
||||
---
|
||||
|
||||
## 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.** 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
|
||||
## 1. Fixtures
|
||||
|
||||
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
|
||||
|
||||
@@ -220,7 +77,6 @@ def app():
|
||||
app = create_app({
|
||||
"TESTING": True,
|
||||
"SECRET_KEY": "test-secret-key",
|
||||
# override database URI for tests, etc.
|
||||
})
|
||||
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.
|
||||
|
||||
@@ -351,7 +207,7 @@ def test_format_currency_rejects_negative(amount):
|
||||
|
||||
---
|
||||
|
||||
## 9. Mocking
|
||||
## 4. Mocking
|
||||
|
||||
### 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:
|
||||
|
||||
@@ -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