conftest.py, project layouts, pyproject.toml — discovery configuration
В этом уроке — infrastructure layer pytest: где хранить shared fixtures, как организовать project structure, и какие настройки vital для production-grade test suite.
В этом уроке:
conftest.py— special file pytest auto-discovers БЕЗ import.- Fixture sharing —
conftest.pypropagates fixtures recursively во все test files под directory. - Project layouts —
src-layoutvsflat-layout(pros/cons). pyproject.toml [tool.pytest.ini_options]— modern config (vs legacypytest.ini).testpaths,python_files,markers,addopts— main config keys.- Browser challenge framing — Pattern 4 string-analysis (triple-quote conftest content).
conftest.py — special file pytest auto-discovers
conftest.py — magic file name pytest автоматически загружает при collection. No explicit import needed:
project/
├── pyproject.toml
├── tests/
│ ├── conftest.py ← shared fixtures для tests/
│ ├── test_users.py ← uses fixtures from tests/conftest.py automatically
│ ├── test_orders.py ← uses fixtures from tests/conftest.py automatically
│ └── integration/
│ ├── conftest.py ← additional fixtures для tests/integration/
│ └── test_api.py ← inherits fixtures from BOTH conftest.py files
└── src/mypkg/...
tests/conftest.py:
# tests/conftest.py
import pytest
@pytest.fixture(scope='session')
def database():
"""Shared session-scoped DB fixture."""
db = Database(':memory:')
db.run_migrations()
yield db
db.drop_all()
@pytest.fixture
def alice(database):
"""Per-test Alice user fixture."""
user = database.insert_user('alice')
yield user
database.delete_user(user.id)
tests/test_users.py:
# tests/test_users.py
# NO import from conftest needed — pytest auto-injects
def test_alice_exists(alice):
assert alice.name == 'alice'
tests/test_users.py использует fixture alice без import — pytest автоматически discovers и injects.
Recursive propagation: tests/integration/test_api.py имеет access к обеим:
- Fixtures из
tests/conftest.py(parent directory). - Fixtures из
tests/integration/conftest.py(own directory).
Override — если в tests/integration/conftest.py есть fixture с тем же именем, она shadows parent’s fixture для тестов в этой directory.
Cite: pytest 8 docs — conftest.py; _pytest/config/__init__.py — discovery machinery.
Project layouts — src-layout vs flat-layout
Two main project structures для Python projects:
src-layout (recommended для new projects)
project/
├── pyproject.toml
├── src/
│ └── mypkg/
│ ├── __init__.py
│ ├── auth.py
│ └── models.py
└── tests/
├── conftest.py
├── test_auth.py
└── test_models.py
Pros:
- Forced installation — нельзя случайно
import mypkgбезpip install -e .. Tests always run против installed package, не source tree. Это catches missing manifest entries (e.g., forgot to include resource file in package data). - Clean separation — production code в
src/, tests вtests/. Build tools (poetry, hatch) предполагаютsrc/layout.
Cons:
- Editable install required —
pip install -e .before first test run. Adds setup step. - Slight cognitive overhead —
import mypkglooks forsrc/mypkg, not project root.
flat-layout (older, simpler)
project/
├── pyproject.toml
├── mypkg/
│ ├── __init__.py
│ ├── auth.py
│ └── models.py
└── tests/
├── conftest.py
└── test_auth.py
Pros:
- No install needed — pytest finds
mypkgautomatically (Python adds CWD to sys.path). - Simpler для small projects / scripts / prototypes.
Cons:
- Hidden bugs — local development imports differ from installed package imports.
- Tools like tox / hatch may не работают cleanly без extra config.
Pragmatic rule: src-layout для production projects (libraries, services). flat-layout ok для scripts, notebooks, learning projects. Modern toolchain (poetry, hatch, uv) all default to src-layout.
Cite: pytest 8 docs — good integration practices; PyPA src-layout vs flat-layout.
pyproject.toml [tool.pytest.ini_options] — modern config
Modern config goes в pyproject.toml (PEP 518 standard для Python project metadata):
# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mypkg"
version = "0.1.0"
dependencies = [
"requests>=2.31",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks integration tests requiring external services",
]
addopts = [
"-ra", # show all test outcomes (failed, errors, skipped)
"--strict-markers", # fail if test uses unregistered marker
"--strict-config", # fail if pytest.ini has unknown options
"-W error", # warnings as errors
]
filterwarnings = [
"error",
"ignore::DeprecationWarning:third_party_lib.*",
]
Main config keys:
| Key | Effect |
|---|---|
testpaths | Where pytest searches for tests (limits discovery scope) |
python_files | Test file patterns (default test_*.py) |
python_classes | Test class patterns (default Test*) |
python_functions | Test function patterns (default test_*) |
markers | Registered marker names (avoid typos) |
addopts | Default CLI args appended to every pytest invocation |
filterwarnings | Warning treatment rules |
--strict-markers — production gate. Без него @pytest.mark.slowy (typo) silently не applies. С --strict-markers pytest fails: 'slowy' not found in markers configuration option.
-ra — show extra test summary. Default — pytest hides skipped/xfailed details. -ra includes them — useful для CI logs.
Legacy pytest.ini
Pre-PEP 518 config — отдельный INI file:
# pytest.ini (legacy)
[pytest]
testpaths = tests
python_files = test_*.py
markers =
slow: marks tests as slow
addopts = -ra --strict-markers
Same options, separate file. Pragmatic rule: новые projects — pyproject.toml. Existing — keep pytest.ini если работает.
Cite: pytest 8 docs — config files; PEP 518 — pyproject.toml standard.
Browser challenge framing — Pattern 4 string-analysis
В browser challenges Module 08 для уроков demonstrating pytest syntax (без runtime execution) мы используем Pattern 4 — учащийся пишет conftest.py content в triple-quote string, и solve() верифицирует через substring checks:
# Browser challenge skeleton (py-m08-06-code-1):
conftest_content = '''
# conftest.py — shared fixtures
@pytest.fixture(scope='session')
def db_url():
return 'sqlite:///:memory:'
'''
def solve(_unused):
"""Verify conftest_content имеет required pieces via substring checks."""
checks = [
'@pytest.fixture' in conftest_content,
"scope='session'" in conftest_content,
'def db_url' in conftest_content,
'sqlite:///:memory:' in conftest_content,
]
return f'fixture_decorator={checks[0]}, scope_session={checks[1]}, fn_name={checks[2]}, db_url_value={checks[3]}'
Critical design — conftest_content — это string, не actual Python code. Pyodide не executes import pytest — import pytest lives только в triple-quote text (если бы было). Validator (validate-allowed-imports.cjs) checks for line-anchored import pytest patterns ((?:^|\\n)\\s*import\\s+pytest) — но мы скорее избегаем literal import pytest в triple-quote, проверяя '@pytest.fixture' substring instead. Это sidesteps potential validator collision.
Параллель с pytest:
| Browser challenge (Pattern 4) | Локальный pytest equivalent |
|---|---|
conftest_content = '''...''' (string) | Файл conftest.py (executable) |
'@pytest.fixture' in conftest_content (substring check) | Pytest discovery loads conftest.py automatically |
solve() returns booleans | pytest -v собирает fixtures для tests |
Mental model: browser challenge педагогически тренирует syntax recognition — учащийся пишет canonical conftest.py code, even если runtime — string-analysis. Local pytest — drop-in replacement (drop string quotes → save as conftest.py → done).
Cite: 67-RESEARCH.md Pattern 4 lines 707-724.
Recipe — production conftest.py + pyproject.toml
End-to-end production setup:
pyproject.toml — config:
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"integration: integration tests requiring external services",
"slow: tests that take >5s",
]
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
]
tests/conftest.py — shared fixtures:
# tests/conftest.py
import pytest
from mypkg.database import Database
from mypkg.user_service import UserService
@pytest.fixture(scope='session')
def app_config():
"""Read-only test config."""
return {
'db_url': 'sqlite:///:memory:',
'log_level': 'DEBUG',
}
@pytest.fixture(scope='session')
def db_schema(app_config):
"""One DB schema per session — expensive setup."""
db = Database(app_config['db_url'])
db.run_migrations()
yield db
db.drop_all()
@pytest.fixture
def user_service(db_schema):
"""Per-test UserService — fresh state."""
return UserService(db_schema)
@pytest.fixture
def alice(user_service):
"""Per-test Alice fixture."""
user = user_service.register('[email protected]', 'password')
yield user
user_service.delete(user.id)
tests/test_user_service.py — uses fixtures без import:
# tests/test_user_service.py
def test_alice_can_login(alice, user_service):
"""Alice (created via fixture) can authenticate."""
auth = user_service.authenticate(alice.email, 'password')
assert auth.is_valid is True
@pytest.mark.integration
def test_alice_via_real_smtp(alice):
"""Tagged как integration — `pytest -m integration` runs only these."""
...
tests/integration/conftest.py — additional fixtures для integration tests:
# tests/integration/conftest.py
import pytest
@pytest.fixture(scope='session')
def real_smtp_server():
"""Real SMTP for integration — additional setup."""
server = MockSMTPServer.start_subprocess()
yield server
server.stop()
Что мы получили:
- Layered fixtures — session-scoped (config, schema) + function-scoped (services, users). Recall lesson 02 scope hierarchy.
- Recursive auto-discovery —
tests/integration/test_X.pyимеет access к обеим conftest files. - Markers +
--strict-markers— typo-safe categorization. - CLI ergonomics —
pytest -m integrationruns только integration tests;pytest -m "not slow"skips slow.
Это — production-grade test infrastructure.
Brief — pytest_plugins для plugin loading
В conftest.py или test files можно declare pytest_plugins list — pytest loads them automatically:
# tests/conftest.py
pytest_plugins = [
'pytest_asyncio', # для async tests
'tests.fixtures.db', # custom fixtures module
]
Это alternative — pip install pytest-asyncio + auto-discovery. Use case: organize fixtures в submodules, plugin loading via config.
Cite: pytest 8 docs — pytest_plugins.
Ключевые выводы
conftest.py— special pytest file, auto-discovered БЕЗ import. Fixtures defined here propagate recursively ко всем test files под directory. Multiple conftest.py — child shadows parent fixture при name collision.- Project layouts:
src-layout(recommended) —src/mypkg/+tests/, forced editable install caught manifest issues.flat-layout— simpler, ok для scripts/prototypes. pyproject.toml [tool.pytest.ini_options]— modern config (PEP 518 standard). Replaces legacypytest.ini. Main keys:testpaths,python_files,markers,addopts.--strict-markers— production gate (catches typos в@pytest.mark.X);-ra— show extra test summary в CI logs.- Browser challenge Pattern 4 (string-analysis triple-quote) — pedagogical technique для demonstrating pytest syntax без runtime execution.
conftest_content = '''...'''+ substring checks. Same mental model — drop quotes для local file. - Cite: pytest 8 docs — conftest.py / good integration practices;
_pytest/config/__init__.pydiscovery machinery.
Дальше — summary + CI bridge (урок 07): module recap + Run-on-Your-Machine для coverage.py (pip install coverage + coverage run -m pytest + coverage report) + forward-link Phase 69 (Production Skills CI integration).