Pytest Architecture, Test Discovery & conftest.py
Enterprise software quality in Python relies on pytest. Unlike legacy unittest (which requires class inheritance), pytest provides a clean function-based testing model powered by AST assertion rewriting, hierarchical fixture inheritance via conftest.py, and test filtering markers (@pytest.mark).
This chapter details pytest test discovery rules, conftest.py hierarchy resolution, AST assertion rewriting, and custom test selection markers.
1. Test Discovery Rules & Execution Cycle
When you invoke pytest without arguments, it executes an automated 4-step discovery pipeline:
Pytest Test Discovery & Execution Pipeline:
[ Pytest Invocation ]
|
v (1. Recursively scans current directory tree)
[ Matches File Patterns: test_*.py or *_test.py ]
|
v (2. Imports test modules & inspects conftest.py files)
[ Matches Test Callables: test_*() functions or Test* class methods ]
|
v (3. Executes AST Assertion Rewriting)
[ Runs Test Battery & Reports Results ]2. The conftest.py Scope Hierarchy
conftest.py files act as root configuration and fixture sharing modules for test directories.
Hierarchy Invariant: Fixtures defined in a root
conftest.pyare automatically visible to all sub-directories, while sub-directoryconftest.pyfiles override or extend fixtures for local sub-trees.
conftest.py Directory Hierarchy:
tests/
βββ conftest.py <-- Shared global fixtures (Database session, API client)
βββ unit/
β βββ conftest.py <-- Unit-specific mock fixtures (Overrides DB session with Mock)
β βββ test_service.py
βββ integration/
βββ test_db.py <-- Uses global DB session from root conftest.py3. AST Assertion Rewriting Engine
In standard Python, assert a == b raises a raw AssertionError without showing variable values. pytest uses AST Assertion Rewriting:
- AST Interception: When
pytestimports test files, it intercepts the Abstract Syntax Tree (AST) before compilation. - Rich Expression Inspection: It rewrites
assertAST nodes into detailed diagnostic code that captures and prints intermediate variable values, list diffs, and dictionary comparisons without requiring custom assert methods (self.assertEqual).
# Raw Test Code:
def test_user_age():
user = fetch_user()
assert user.age == 42Pytest Rewritten Failure Output:
def test_user_age():
user = fetch_user()
> assert user.age == 42
E AssertionError: assert 25 == 42
E + where 25 = <User name='Alice' age=25>.age4. Test Filtering Markers (@pytest.mark)
Categorize test suites using custom markers (pytest.ini):
import pytest
@pytest.mark.unit
def test_fast_calculation():
assert 1 + 1 == 2
@pytest.mark.integration
@pytest.mark.slow
def test_database_ingestion():
db.write_heavy_data()# Execute ONLY unit tests in CI/CD pipeline
pytest -m unit
# Execute everything EXCEPT slow integration tests
pytest -m "not slow"