pytest-tests
Configures and runs pytest - the de facto Python test framework with fixture-based dependency injection (`@pytest.fixture` with scopes module/session/function), parametrize for table-driven tests (`@pytest.mark.parametrize`), markers (`@pytest.mark.skip` / `xfail` / `slow`), `conftest.py` for shared fixtures, plugin ecosystem (pytest-cov, pytest-asyncio, pytest-mock, pytest-xdist), `--lf`/`--ff` for fail-loop, coverage gating. Use when working with Python and needing the modern test framework.
pytest-tests
Overview
Per docs.pytest.org/en/stable:
pytest is the de facto Python testing framework. Distinguishes from unittest (stdlib) by: function-style tests (no TestCase class), fixture-based dependency injection, rich plugin ecosystem, parametrize for data-driven tests, intuitive assertions via plain assert (rewritten to provide diff-rich failure output).
Per-framework lifecycle scope: configure / run / fixtures / mocking / coverage / CI. Test code hygiene (assertion quality, AAA structure, mocking anti-patterns) is in test-code-conventions.
When to use
Step 1 - Install
pip install pytest
# Common plugins:
pip install pytest-cov pytest-asyncio pytest-mock pytest-xdistStep 2 - First test
Per pt-docs convention:
# test_sum.py
def sum(a, b):
return a + b
def test_adds_1_and_2():
assert sum(1, 2) == 3pytestpytest auto-discovers via test_*.py / *_test.py filenames and test_* / Test* function/class names.
Step 3 - Configuration
pytest.ini (or pyproject.toml [tool.pytest.ini_options] / setup.cfg [tool:pytest]):
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-ra --strict-markers --strict-config"
markers = [
"slow: marks tests as slow (deselect with -m 'not slow')",
"integration: marks tests requiring DB/external resources",
]--strict-markers rejects undeclared marker names - catches typos like @pytest.mark.skipp (silently skipped before).
Step 4 - Fixtures
import pytest
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn
conn.close()
@pytest.fixture(scope="session")
def app_config():
return load_config()
@pytest.fixture(autouse=True)
def reset_state():
yield
cleanup_after_test()
def test_user_creation(db_connection, app_config):
user = create_user(db_connection, app_config)
assert user.id is not NoneFixture scopes: function (default), class, module, package, session. Choose narrowest scope that doesn't waste setup time.
conftest.py shares fixtures across multiple test files in the same directory (and subdirectories).
Step 5 - Parametrize
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_sum_parametrized(a, b, expected):
assert sum(a, b) == expectedMulti-param multiply (cross-product):
@pytest.mark.parametrize("x", [1, 2, 3])
@pytest.mark.parametrize("y", ['a', 'b'])
def test_combinations(x, y):
# runs 6 times: (1,'a'), (1,'b'), (2,'a'), ...
passStep 6 - Markers + skip/xfail
@pytest.mark.skip(reason="Requires staging DB")
def test_skip_example():
pass
@pytest.mark.skipif(sys.version_info < (3, 11), reason="Python 3.11+ syntax")
def test_modern_syntax():
pass
@pytest.mark.xfail(reason="Known bug; tracked in JIRA-1234")
def test_known_failure():
assert 1 == 2
@pytest.mark.slow
def test_long_running():
passFilter: pytest -m "not slow" skips slow-marked tests.
Step 7 - Mocking with pytest-mock
def test_with_mock(mocker):
mock_api = mocker.patch('mymodule.api_client.fetch')
mock_api.return_value = {'id': 1, 'name': 'Alice'}
result = my_function()
mock_api.assert_called_once_with('/users')
assert result == {'id': 1, 'name': 'Alice'}mocker fixture from pytest-mock wraps unittest.mock.patch with auto-cleanup at test end.
Step 8 - Async (pytest-asyncio)
import pytest
import asyncio
@pytest.mark.asyncio
async def test_async_function():
result = await fetch_data()
assert result == 'expected'
# Or set asyncio_mode = "auto" in pyproject.toml to skip the markerStep 9 - Coverage with pytest-cov
pytest --cov=src --cov-report=term-missing --cov-report=html --cov-report=xml \
--cov-fail-under=80--cov-fail-under=N fails the run if coverage drops below N%.
In pyproject.toml:
[tool.coverage.run]
source = ["src"]
branch = true
omit = ["**/__init__.py", "**/types.py"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
fail_under = 80Step 10 - Fast-feedback flags
pytest --lf # only re-run last-failed tests
pytest --ff # run last-failed first, then the rest
pytest -x # stop on first failure
pytest -k "name_pat" # only tests matching name pattern
pytest -v # verbose
pytest -s # don't capture stdout (see print() output)
pytest -p no:cacheprovider # disable test-cache (CI cache-clean runs)Step 11 - CI integration
- run: pip install -e .[dev]
- run: pytest --cov --cov-report=xml --cov-fail-under=80 --junitxml=junit.xml
- uses: codecov/codecov-action@v4
with: { files: coverage.xml }For parallel execution, add pytest-xdist:
pytest -n auto # uses CPU countAnti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Use setUp / tearDown (TestCase style) instead of fixtures | Loses dependency injection benefits | Use fixtures (Step 4) |
Skip --strict-markers | Typos in markers silently skip tests | Always set in config (Step 3) |
Fixture with scope='session' for stateful resources | State leaks across tests | Function-scope unless setup expensive |
pytest -k 'expr' in CI to skip "slow" tests | Brittle string match | Use -m markers (Step 6) |
Skip --cov-fail-under in CI | Coverage drops silently over time | Always gate coverage (Step 9) |