Advanced Pytest Patterns
Advanced pytest features for robust, maintainable test suites.
When to Use This Skill
Use this skill when... Use python-testing instead when...
Writing fixtures with scopes/factories Basic test structure questions
Parametrizing with complex data Simple assert patterns
Setting up pytest plugins (cov, xdist) Running existing tests
Organizing conftest.py hierarchy Test discovery basics
Async testing with pytest-asyncio Synchronous unit tests
Installation
Core + common plugins
uv add --dev pytest pytest-cov pytest-asyncio pytest-xdist pytest-mock
Configuration (pyproject.toml)
[tool.pytest.ini_options] testpaths = ["tests"] addopts = [ "-v", "--strict-markers", "--tb=short", "-ra", "--cov=src", "--cov-report=term-missing", "--cov-fail-under=80", ] markers = [ "slow: marks tests as slow (deselect with '-m "not slow"')", "integration: marks tests as integration tests", ] asyncio_mode = "auto"
[tool.coverage.run] branch = true source = ["src"]
[tool.coverage.report] exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "@abstractmethod"]
Fixtures
Scopes and Lifecycle
import pytest from typing import Generator
function (default) - fresh per test
@pytest.fixture def db() -> Generator[Database, None, None]: database = Database(":memory:") database.create_tables() yield database database.close()
session - shared across all tests
@pytest.fixture(scope="session") def app(): return create_app("testing")
autouse - applies automatically
@pytest.fixture(autouse=True) def reset_state(): clear_cache() yield
Scope Lifetime
function
Each test (default)
class
Each test class
module
Each test file
session
Entire test run
Parametrized Fixtures
@pytest.fixture(params=["sqlite", "postgres", "mysql"]) def database_backend(request) -> str: return request.param # Test runs 3 times
Indirect parametrization
@pytest.fixture def user(request) -> User: return User(**request.param)
@pytest.mark.parametrize("user", [ {"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, ], indirect=True) def test_user_validation(user: User): assert user.name
Factory Pattern
@pytest.fixture def user_factory() -> Callable[[str], User]: created: list[User] = [] def _create(name: str, **kwargs) -> User: user = User(name=name, **kwargs) created.append(user) return user yield _create for u in created: u.delete()
Markers
Built-in markers
@pytest.mark.skip(reason="Not implemented") @pytest.mark.skipif(sys.version_info < (3, 12), reason="Requires 3.12+") @pytest.mark.xfail(reason="Known bug #123") @pytest.mark.timeout(10)
Parametrize
@pytest.mark.parametrize("input,expected", [ pytest.param(2, 4, id="two"), pytest.param(3, 9, id="three"), pytest.param(-2, 4, id="negative"), ]) def test_square(input: int, expected: int): assert input ** 2 == expected
Run by marker
pytest -m unit # Only unit tests pytest -m "not slow" # Skip slow tests pytest -m "integration and not slow" # Combine markers
Key Plugins
Plugin Purpose Key Command
pytest-cov Coverage pytest --cov=src --cov-report=term-missing
pytest-xdist Parallel pytest -n auto
pytest-asyncio Async tests asyncio_mode = "auto" in config
pytest-mock Mocking mocker fixture
pytest-timeout Timeouts @pytest.mark.timeout(10)
Async Testing (pytest-asyncio)
@pytest.mark.asyncio async def test_async_function(): result = await fetch_data() assert result is not None
@pytest.fixture async def async_client() -> AsyncGenerator[AsyncClient, None]: async with AsyncClient() as client: yield client
Mocking (pytest-mock)
def test_with_mock(mocker): mock_api = mocker.patch("myapp.external.api_call") mock_api.return_value = {"data": "test"} result = my_function() assert result["data"] == "test" mock_api.assert_called_once()
Running Tests
Execution
pytest # All tests pytest tests/test_models.py::test_user # Specific test pytest -k "user and not slow" # Pattern matching
Parallel
pytest -n auto # All CPUs pytest -n 4 # 4 workers
Coverage
pytest --cov=src --cov-report=html --cov-report=term-missing
Failed tests
pytest --lf # Last failed only pytest --ff # Failed first pytest -x # Stop on first failure pytest --maxfail=3 # Stop after 3
Debugging
pytest -x --pdb # Debug on failure pytest -s # Show print output pytest --collect-only # Dry run
CI Integration
.github/workflows/test.yml
- name: Run tests
run: |
uv run pytest
--cov=src
--cov-report=xml
--cov-report=term-missing
--junitxml=test-results.xml
Agentic Optimizations
Context Command
Quick check pytest -x --tb=short -q
Fail fast pytest -x --maxfail=1 --tb=short
Parallel fast pytest -n auto -x --tb=short -q
Specific test pytest tests/test_foo.py::test_bar -v
By marker pytest -m "not slow" -x --tb=short
Coverage check pytest --cov=src --cov-fail-under=80 -q
CI mode pytest --junitxml=results.xml --cov-report=xml -q
Last failed pytest --lf --tb=short
Debug pytest -x --pdb -s
For detailed patterns on conftest.py hierarchy, async testing, test organization, and common patterns, see REFERENCE.md.