# 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 ```python # tests/conftest.py import pytest @pytest.fixture def sample_user(): return User(name="alice", email="alice@example.com") ``` ```python # tests/test_models.py def test_user_display_name(sample_user): assert sample_user.display_name == "alice" ``` ### Yield fixtures for setup/teardown ```python @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 suite - `tests/api/conftest.py` — fixtures specific to API tests - Fixtures in a `conftest.py` are available to all tests in that directory and below --- ## 2. Flask/Quart Test Patterns ### App factory fixture ```python # 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 ```python 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 ```python 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 ```python 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 ```python def test_db_query_within_app_context(app): with app.app_context(): users = User.query.all() assert len(users) >= 0 ``` ### Testing with request context ```python def test_url_generation(app): with app.test_request_context(): assert url_for("index") == "/" ``` ### Quart async tests For Quart applications, use `pytest-asyncio`: ```python 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 ```python @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 ```python @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) ```python 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 ```python 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 ```python 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`, not `assert 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`, or `try/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 ```bash uv add --dev pytest-cov pytest --cov=src --cov-report=term-missing -v ``` Report untested lines and suggest tests for them.