Test suites, async tests, and mocking
Organize pytest suites with conftest.py, async fixtures, and plugins. Test HTTP and database boundaries and patch dependencies safely.
Test layout and shared setup#
Put tests near related behavior and keep fixture visibility predictable.
A conftest.py makes fixtures available in its directory and child directories. A tests/unit/conftest.py does not affect sibling tests/integration tests. Tests can import ordinary Python helpers, but fixture discovery is by fixture name. Use --fixtures-per-test to see which fixtures each collected test uses.
project/
pyproject.toml
app/
billing.py
tests/
conftest.py # shared client and factories
unit/
test_billing.py
integration/
conftest.py # DB fixture only for integration tests
test_checkout.pyFor deterministic tests, isolate external systems and clean up state. Avoid sharing a mutable global or a session-scoped database transaction across tests. Organize by feature and test layer; unit tests assert pure behavior, integration tests verify real boundaries.
python -m pytest --collect-only -q tests/
python -m pytest --fixtures-per-test tests/unit/test_billing.py
python -m pytest --setup-plan tests/integration/An ordinary Test* class groups tests and can use fixtures in methods. Do not add __init__ to the class; pytest cannot collect such a test class normally.
class TestTotals:
def test_empty(self):
assert sum([]) == 0
def test_multiple(self, cart):
cart["items"] = [1, 2, 3]
assert sum(cart["items"]) == 6Async tests and fixtures#
pytest core does not run async def tests by itself; install an async test plugin.
pytest-asyncio runs asyncio tests; pytest.mark.asyncio marks a coroutine test in strict mode. In auto mode it discovers asyncio tests automatically. In strict mode, decorate async fixtures with @pytest_asyncio.fixture. Use AsyncMock for awaited dependencies and assert that the awaited call happened.
python -m pip install pytest-asyncio
python -m pytest tests/test_async_jobs.py -q
python -m pytest --asyncio-mode=autoimport pytest
import pytest_asyncio
from unittest.mock import AsyncMock
@pytest_asyncio.fixture
async def inbox():
queue = AsyncMock()
queue.receive.return_value = {"id": 11, "attempts": 0}
return queue
@pytest.mark.asyncio
async def test_receive_one_message(inbox):
result = await inbox.receive()
assert result["id"] == 11
inbox.receive.assert_awaited_once_with()Return a value from fixtures that need no cleanup. Use yield for async resources, and configure test and fixture loop scopes when connections are tied to an event loop. These settings belong to the async plugin.
@pytest_asyncio.fixture
async def connection():
conn = await open_connection()
try:
yield conn
finally:
await conn.close()[tool.pytest.ini_options]
asyncio_mode = "strict"
asyncio_default_test_loop_scope = "function"
asyncio_default_fixture_loop_scope = "function"pytest-trio or AnyIO's plugin). Mixing loop policies and sharing resources between event loops causes confusing failures.Use popular plugins#
Plugins add fixtures, commands and hooks; check each plugin's docs before using its options.
pytest auto-loads installed plugin entry points by default. pytest --trace-config shows the loaded plugins. Disable one with -p no:plugin_name, or set PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 when debugging interference. The pip package name and pytest plugin name can differ.
python -m pip install pytest-mock pytest-cov pytest-xdist pytest-asyncio
python -m pytest --trace-config
python -m pytest -p no:cacheproviderpytest-mock supplies mocker; pytest-cov supplies --cov; pytest-xdist supplies -n. These are separate packages, so install them before using their features. pytest-django offers Django settings, client and DB markers; pytest-httpx can fake HTTPX calls. Each has its own behavior and version requirements.
def test_charge_call(mocker):
charge = mocker.patch("app.billing.gateway.charge", autospec=True)
charge.return_value = "receipt-9"
assert billing.checkout({"id": 9, "total_cents": 80})["receipt"] == "receipt-9"
charge.assert_called_once_with(80)python -m pytest --cov=app --cov-report=term-missing
python -m pytest -n auto
python -m pytest --cov=app --cov-branch --cov-report=htmlxdist workers are separate processes: each gets its own process memory and fixture instances. Make test databases, ports, and temporary resources worker-safe. Coverage measures executed lines or branches, not the quality of assertions.
HTTP and service boundaries#
Use a framework test client for routes, replace external calls at the boundary, and assert both response and side effect.
Client fixtures are supplied by the web framework or by your project. The example below uses Flask's test client; pytest core does not provide client. For FastAPI, Django or other frameworks, use their documented test clients and DB integration.
# conftest.py
import pytest
from app.web import create_app
@pytest.fixture
def client():
app = create_app(testing=True)
return app.test_client()
# test_api.py
def test_create_order_requires_email(client):
response = client.post("/orders", json={"items": ["A-1"]})
assert response.status_code == 400
assert response.json["code"] == "email_required"A broader integration test can verify the real database transaction. Reset or roll back state after each test and create only the rows the test needs. Avoid a shared external service in unit tests. Test one known failure response as well as the happy path.
@pytest.fixture
def db_session(engine):
connection = engine.connect()
transaction = connection.begin()
session = Session(bind=connection)
try:
yield session
finally:
session.close()
transaction.rollback()
connection.close()The transaction pattern depends on your ORM and app wiring; nested transactions may be needed when the code under test commits. Prefer the framework's integration plugin when it already handles transactional tests.
Mocking boundaries safely#
A useful fake matches the relevant contract; a precise spy verifies an important interaction.
Prefer autospec=True for patched callables so wrong argument names fail early. Mock(spec=Type) restricts exposed attributes; spec_set also blocks assigning unknown attributes. Use side_effect to raise or return a sequence. An async function should be mocked with AsyncMock.
from unittest.mock import Mock, AsyncMock
def test_retry_after_timeout(mocker):
send = mocker.patch("app.worker.gateway.send", autospec=True)
send.side_effect = [TimeoutError("late"), {"id": "ok"}]
result = worker.dispatch_with_retry({"id": 5})
assert result == {"id": "ok"}
assert send.call_count == 2
@pytest.mark.asyncio
async def test_async_dispatch(monkeypatch):
send = AsyncMock(return_value={"id": "ok"})
monkeypatch.setattr(async_worker.gateway, "send", send)
assert await async_worker.dispatch({"id": 5}) == {"id": "ok"}
send.assert_awaited_once_with({"id": 5})mocker comes from pytest-mock; use unittest.mock.patch without that plugin. Patch where the code looks up a name. Keep a real integration test for behavior that mocks cannot verify.
Help improve CodeRef
Allow Google Analytics to measure page visits and use of the guides? It uses cookies after you accept. Your choice won’t affect access to the site. Privacy notice