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 @given constraints — 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 .