Advanced testing cheatsheet. Pair with Cheatsheet 07 — pytest .
unittest.mock basics
from unittest.mock import MagicMock, AsyncMock, patch
m = MagicMock()
m.foo.return_value = 42
m.foo() # 42
m.foo.assert_called_once()
m.foo.assert_called_with()
# Async
am = AsyncMock()
am.foo.return_value = 42
await am.foo()
patch (replace during test)
@patch("myapp.module.func")
def test_x(mock_func):
mock_func.return_value = "patched"
result = my_code()
mock_func.assert_called()
# Or context manager
def test_y():
with patch("myapp.module.func") as m:
m.return_value = "x"
...
patch.object
@patch.object(MyClass, "method")
def test_method(mock_method):
mock_method.return_value = "x"
side_effect
m.side_effect = [1, 2, 3] # returns 1, 2, 3 on consecutive calls
m.side_effect = ValueError # raises on call
m.side_effect = lambda x: x + 1
monkeypatch (pytest fixture)
def test_env(monkeypatch):
monkeypatch.setenv("API_KEY", "test")
monkeypatch.setattr("module.func", lambda: "x")
monkeypatch.delattr("module.attr", raising=False)
Auto-undone after test.
freezegun (mock time)
from freezegun import freeze_time
@freeze_time("2026-01-15")
def test_today():
assert datetime.now().date() == date(2026, 1, 15)
# Tick time
@freeze_time("2026-01-15", tick=True)
def test_with_tick():
# time advances normally; just starts at a known point
...
Snapshot testing (syrupy)
uv add --dev syrupy
def test_render(snapshot):
result = render_page(...)
assert result == snapshot
pytest --snapshot-update # regenerate
hypothesis (property-based)
uv add --dev hypothesis
from hypothesis import given, strategies as st
@given(st.integers(), st.integers())
def test_add_commutative(a, b):
assert add(a, b) == add(b, a)
@given(st.text(min_size=1))
def test_uppercase_invertible(s):
assert s.upper().lower().casefold() == s.casefold()
Generates random inputs; finds edge cases.
factory_boy
import factory
from src.app.models import User
class UserFactory(factory.Factory):
class Meta:
model = User
id = factory.Sequence(lambda n: n)
email = factory.Sequence(lambda n: f"user{n}@example.com")
name = factory.Faker("name")
# Use
u = UserFactory.build() # not saved
u = UserFactory() # may save (depends on Meta)
SubFactory
class PostFactory(factory.Factory):
class Meta: model = Post
title = factory.Faker("sentence")
author = factory.SubFactory(UserFactory)
polyfactory (Pydantic-aware)
from polyfactory.factories.pydantic_factory import ModelFactory
class UserFactory(ModelFactory[User]):
__model__ = User
u = UserFactory.build()
Auto-generates Pydantic models from their schema.
Mocking HTTP
uv add --dev respx
import respx, httpx
@respx.mock
async def test_call():
respx.get("https://api.example.com/users/1").mock(
return_value=httpx.Response(200, json={"id": 1})
)
r = await my_code()
assert r["id"] == 1
Or fixture:
@pytest.fixture
def mock_api():
with respx.mock(base_url="https://api.example.com") as m:
m.get("/users").respond(json=[...])
yield m
Approx (float comparison)
assert 0.1 + 0.2 == pytest.approx(0.3)
assert vector == pytest.approx([1.0, 2.0, 3.0], rel=1e-3)
Comparing dataclass / Pydantic
@dataclass
class Point: x: int; y: int
assert Point(1, 2) == Point(1, 2) # value equality
Pydantic models compare by field values.
Test temporal queries
@freeze_time("2026-01-01")
def test_expires(db_session):
user = UserFactory.build()
db_session.add(user); db_session.commit()
with freeze_time("2026-04-01"):
assert user.is_expired
Parameterize with marks
@pytest.mark.parametrize("payload, expected", [
pytest.param({"a": 1}, True, id="happy"),
pytest.param({"a": -1}, False, id="negative", marks=pytest.mark.xfail),
])
def test_(payload, expected): ...
Conftest discovery
tests/
├── conftest.py # global fixtures
├── api/
│ ├── conftest.py # api-specific fixtures
│ └── test_users.py
Fixtures in tests/conftest.py available everywhere; in api/conftest.py only in api/.
tox / nox (multi-env)
uv add --dev nox
# noxfile.py
import nox
@nox.session(python=["3.12", "3.13"])
def tests(session):
session.install(".[dev]")
session.run("pytest")
nox # runs all sessions
nox -s tests # one session
Coverage tips
pytest --cov=src --cov-branch --cov-report=html
Branch coverage catches missing else-branches.
Common mistakes
- Mocking too deep — test setup becomes brittle.
- Patching where the function is defined, not where it’s used.
- Property-based test without
@givenconstraints — runs forever. - Snapshot tests without review on update — captures bugs.
Read this next
If you want my testing harness (mocks + factories + freezegun), it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .