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.py are automatically visible to all sub-directories, while sub-directory conftest.py files 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.py

3. 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 pytest imports test files, it intercepts the Abstract Syntax Tree (AST) before compilation.
  • Rich Expression Inspection: It rewrites assert AST 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 == 42
Pytest 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>.age

4. 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"
Display Options
Appearance
Text Size
100%