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.
7.1 KiB
Pytest TDD Workflow — Advanced
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
- 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. Fixtures
Use fixtures for setup code that multiple tests share.
Basic fixture
# tests/conftest.py
import pytest
@pytest.fixture
def sample_user():
return User(name="alice", email="alice@example.com")
# tests/test_models.py
def test_user_display_name(sample_user):
assert sample_user.display_name == "alice"
Yield fixtures for setup/teardown
@pytest.fixture
def db_session():
session = create_session()
yield session
session.rollback()
session.close()
Fixture scope
| Scope | Lifetime | Use when |
|---|---|---|
"function" (default) |
Each test | Most cases — tests stay isolated |
"class" |
Each test class | Grouping related tests with shared setup |
"module" |
Each test file | Expensive setup shared across a file |
"session" |
Entire test run | Very expensive one-time setup (e.g. database creation) |
Prefer function scope. Only widen scope when setup is genuinely expensive and tests do not mutate shared state.
conftest.py organization
tests/conftest.py— fixtures used across the entire test suitetests/api/conftest.py— fixtures specific to API tests- Fixtures in a
conftest.pyare available to all tests in that directory and below
2. Flask/Quart Test Patterns
App factory fixture
# tests/conftest.py
import pytest
from myapp import create_app
@pytest.fixture
def app():
app = create_app({
"TESTING": True,
"SECRET_KEY": "test-secret-key",
})
yield app
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
Testing routes
def test_homepage_returns_200(client):
response = client.get("/")
assert response.status_code == 200
def test_homepage_contains_title(client):
response = client.get("/")
assert b"Welcome" in response.data
Testing POST requests and forms
def test_login_with_valid_credentials(client):
response = client.post("/login", data={
"username": "alice",
"password": "correct-password",
}, follow_redirects=True)
assert response.status_code == 200
Testing JSON APIs
def test_api_returns_user_list(client):
response = client.get("/api/users")
assert response.status_code == 200
data = response.get_json()
assert isinstance(data, list)
Testing with application context
def test_db_query_within_app_context(app):
with app.app_context():
users = User.query.all()
assert len(users) >= 0
Testing with request context
def test_url_generation(app):
with app.test_request_context():
assert url_for("index") == "/"
Quart async tests
For Quart applications, use pytest-asyncio:
import pytest
@pytest.fixture
def app():
app = create_app({"TESTING": True})
return app
@pytest.fixture
def client(app):
return app.test_client()
@pytest.mark.asyncio
async def test_homepage(client):
response = await client.get("/")
assert response.status_code == 200
3. Parametrize
Use @pytest.mark.parametrize to test multiple inputs without duplicating test functions.
Basic parametrize
@pytest.mark.parametrize("input_val, expected", [
("alice", True),
("", False),
("a" * 256, False),
("bob@", False),
])
def test_is_valid_username(input_val, expected):
assert is_valid_username(input_val) == expected
Testing edge cases systematically
Think about these categories for each function:
- Valid inputs — normal cases that should succeed
- Boundary values — empty strings, zero, max length, off-by-one
- Invalid inputs — wrong types, malformed data, None
- Error cases — inputs that should raise specific exceptions
@pytest.mark.parametrize("amount, expected", [
(100, "$1.00"), # normal
(0, "$0.00"), # zero
(1, "$0.01"), # minimum
(99999, "$999.99"), # large value
])
def test_format_currency(amount, expected):
assert format_currency(amount) == expected
@pytest.mark.parametrize("amount", [-1, -100])
def test_format_currency_rejects_negative(amount):
with pytest.raises(ValueError):
format_currency(amount)
4. Mocking
When to mock
- Mock: external services (APIs, email, payment), system clock, file system operations, slow or nondeterministic operations.
- Don't mock: your own code under test, data structures, pure functions. Prefer real objects when practical.
monkeypatch (preferred for simple cases)
def test_get_api_data(monkeypatch):
def mock_get(url):
return MockResponse(json_data={"result": "ok"})
monkeypatch.setattr("myapp.services.requests.get", mock_get)
data = get_api_data()
assert data["result"] == "ok"
monkeypatch for environment variables
def test_config_reads_env(monkeypatch):
monkeypatch.setenv("SECRET_KEY", "test-key")
config = load_config()
assert config.secret_key == "test-key"
unittest.mock for complex cases
from unittest.mock import patch, MagicMock
@patch("myapp.services.send_email")
def test_registration_sends_welcome_email(mock_send):
register_user("alice@example.com")
mock_send.assert_called_once_with(
to="alice@example.com",
subject="Welcome",
)
5. Test Quality Checklist
After completing a set of tests, review against this checklist:
- Every public function/route has at least one test.
- Tests are isolated — no test depends on another test's state or execution order.
- No hardcoded paths or ports — use fixtures and configuration.
- Assertions are specific — test exact values, not just truthiness (
assert x == 5, notassert x). - Error paths are tested — not just the happy path.
- Test names describe behavior — someone reading only test names understands what the code does.
- Fixtures are minimal — each fixture sets up only what is needed, no "god fixtures" with everything.
- No logic in tests — tests should not contain
if,for, ortry/except. Each test is a straight line. - Mocks are focused — only mock what is necessary. Over-mocking makes tests brittle.
- Tests run fast — the full suite completes in seconds. Slow tests indicate missing mocks or unnecessary I/O.
Running with coverage
uv add --dev pytest-cov
pytest --cov=src --cov-report=term-missing -v
Report untested lines and suggest tests for them.