pytest — Deep Dive
Playground note: pytest requires the
pytestbinary and test discovery context — run locally withpytest. Code blocks below are verified withpython3where applicable; full test runs require a local environment.
pytest vs Jest: conceptual map
Section titled “pytest vs Jest: conceptual map”| Jest concept | pytest equivalent |
|---|---|
describe block | module or class grouping (optional) |
it / test | function named test_* |
beforeEach / afterEach | fixture with yield |
beforeAll / afterAll | session/module-scoped fixture |
it.each / test.each | @pytest.mark.parametrize |
expect(x).toBe(y) | assert x == y (with introspection) |
jest.fn() / jest.mock() | monkeypatch fixture / unittest.mock |
Fixtures: the pytest superpower
Section titled “Fixtures: the pytest superpower”A fixture is a function decorated with @pytest.fixture. pytest injects it into any test that declares it as a parameter. It is the equivalent of Jest’s beforeEach — but more powerful because fixtures are composable: a fixture can depend on other fixtures.
// Jest — beforeEach for setupdescribe("UserService", () => { let db: Database; let service: UserService;
beforeEach(async () => { db = await Database.connect(":memory:"); service = new UserService(db); });
afterEach(async () => { await db.close(); });
it("creates a user", async () => { const user = await service.create({ name: "Alice" }); expect(user.id).toBeDefined(); });});# pytest — fixture with yield (setup + teardown)import pytest
@pytest.fixturedef db(): connection = {"connected": True, "data": {}} yield connection # test receives this value connection["connected"] = False # teardown after yield
@pytest.fixturedef user_service(db): # fixture depending on fixture return {"db": db, "users": db["data"]}
def test_create_user(user_service): user_service["users"]["alice"] = {"name": "Alice"} assert "alice" in user_service["users"]
def test_db_connected(db): assert db["connected"] is TrueRun locally:
pip install pytestpytest test_users.py -vFixture scopes
Section titled “Fixture scopes”Fixtures default to scope="function" (recreated per test). Wider scopes avoid expensive setup:
@pytest.fixture(scope="module") # once per test moduledef db_connection(): conn = create_db_connection() yield conn conn.close()
@pytest.fixture(scope="session") # once for the entire test rundef auth_token(): return get_test_token()| Scope | Created |
|---|---|
function (default) | Before every test |
class | Once per test class |
module | Once per .py file |
session | Once per pytest invocation |
parametrize: data-driven tests
Section titled “parametrize: data-driven tests”@pytest.mark.parametrize is Jest’s it.each — run the same test with multiple input/output pairs.
// Jest — test.eachtest.each([ [2, 3, 5], [0, 0, 0], [-1, 1, 0],])("add(%i, %i) = %i", (a, b, expected) => { expect(add(a, b)).toBe(expected);});import pytest
def add(a: int, b: int) -> int: return a + b
@pytest.mark.parametrize("a,b,expected", [ (2, 3, 5), (0, 0, 0), (-1, 1, 0), (10, -5, 5),])def test_add(a, b, expected): assert add(a, b) == expectedCombine parametrize with fixtures for full combinatorial testing:
@pytest.mark.parametrize("role", ["admin", "user", "guest"])def test_permissions(role, user_service): # user_service fixture injected; role parametrized perms = user_service.get_permissions(role) assert isinstance(perms, list)Assertions: plain assert with introspection
Section titled “Assertions: plain assert with introspection”Unlike Jest’s .toBe(), .toEqual(), .toContain() chain, pytest uses a plain assert statement. It rewrites the assertion at collection time and shows a detailed diff on failure — no matcher library needed.
// Jest assertionsexpect(user.name).toBe("Alice");expect(users).toHaveLength(3);expect(users).toContain("Bob");expect(order.total).toBeGreaterThan(0);expect(fn).toThrow(TypeError);# pytest — plain assert, rich diffs on failureassert user["name"] == "Alice"assert len(users) == 3assert "Bob" in usersassert order["total"] > 0
# Testing exceptionsimport pytestwith pytest.raises(ValueError, match="negative"): parse_price(-5)When an assertion fails, pytest shows the actual values, the full diff for containers, and the expression tree — far more detail than you get with a plain AssertionError.
conftest.py: shared fixtures
Section titled “conftest.py: shared fixtures”Place shared fixtures in conftest.py at any directory level. pytest discovers them automatically — no import needed.
flowchart TD tests["tests/"] rootcf["conftest.py — fixtures for ALL tests below"] unit["unit/"] unitcf["conftest.py — fixtures only for unit/ tests"] testmath["test_math.py"] integration["integration/"] testapi["test_api.py"] tests --> rootcf tests --> unit tests --> integration unit --> unitcf unit --> testmath integration --> testapi
import pytest
@pytest.fixture(scope="session")def api_client(): from myapp import create_app app = create_app(testing=True) with app.test_client() as client: yield clientmonkeypatch: the built-in mock
Section titled “monkeypatch: the built-in mock”monkeypatch is a built-in fixture for patching objects, environment variables, and files — the equivalent of jest.spyOn or jest.mock.
def test_calls_external_api(monkeypatch): def fake_fetch(url): return {"status": 200, "data": "mocked"}
monkeypatch.setattr("mymodule.fetch", fake_fetch) result = mymodule.process() assert result["status"] == 200