Skip to content

pytest — Deep Dive

Playground note: pytest requires the pytest binary and test discovery context — run locally with pytest. Code blocks below are verified with python3 where applicable; full test runs require a local environment.

Jest conceptpytest equivalent
describe blockmodule or class grouping (optional)
it / testfunction named test_*
beforeEach / afterEachfixture with yield
beforeAll / afterAllsession/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

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.

TypeScript
// Jest — beforeEach for setup
describe("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();
});
});
Python
# pytest — fixture with yield (setup + teardown)
import pytest
@pytest.fixture
def db():
connection = {"connected": True, "data": {}}
yield connection # test receives this value
connection["connected"] = False # teardown after yield
@pytest.fixture
def 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 True

Run locally:

Terminal window
pip install pytest
pytest test_users.py -v

Fixtures default to scope="function" (recreated per test). Wider scopes avoid expensive setup:

@pytest.fixture(scope="module") # once per test module
def db_connection():
conn = create_db_connection()
yield conn
conn.close()
@pytest.fixture(scope="session") # once for the entire test run
def auth_token():
return get_test_token()
ScopeCreated
function (default)Before every test
classOnce per test class
moduleOnce per .py file
sessionOnce per pytest invocation

@pytest.mark.parametrize is Jest’s it.each — run the same test with multiple input/output pairs.

TypeScript
// Jest — test.each
test.each([
[2, 3, 5],
[0, 0, 0],
[-1, 1, 0],
])("add(%i, %i) = %i", (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
Python
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) == expected

Combine 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.

TypeScript
// Jest assertions
expect(user.name).toBe("Alice");
expect(users).toHaveLength(3);
expect(users).toContain("Bob");
expect(order.total).toBeGreaterThan(0);
expect(fn).toThrow(TypeError);
Python
# pytest — plain assert, rich diffs on failure
assert user["name"] == "Alice"
assert len(users) == 3
assert "Bob" in users
assert order["total"] > 0
# Testing exceptions
import pytest
with 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.

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
conftest.py fixture scoping by directory
tests/conftest.py
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 client

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
What is the pytest equivalent of Jest's `beforeEach`/`afterEach` for a single test?
Which decorator creates data-driven / parametrized tests in pytest?
What is the default fixture scope in pytest?
Where do you place shared fixtures so pytest discovers them automatically without importing?