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.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Per docs.pytest.org/en/stable:
pytest is the de facto Python test framework. Unlike stdlib unittest, it
uses function-style tests (no TestCase), fixture-based dependency
injection, parametrize for data-driven tests, and plain-assert rewriting
for diff-rich failures.
Lifecycle scope: configure / run / fixtures / mocking / coverage / CI. Test
code hygiene (assertions, AAA, mocking anti-patterns) is in
test-code-conventions (qa-test-review plugin).
pip install pytest
# Common plugins:
pip install pytest-cov pytest-asyncio pytest-mock pytest-xdist# 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.
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).
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).
@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'), ...
pass@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.
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.
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 markerpytest --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%.
Full [tool.coverage.*] config, CI wiring, and parallel execution:
references/coverage-and-ci.md.
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)| 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) |
assert rewriting requires pytest's importer; some patterns
(running tests as scripts) bypass it.unittest-tests,
doctest-tests,
nose2-tests - sister toolstest-code-conventions - test code hygiene