pytest is the de-facto Python testing framework. The official docs are excellent. Also see Real Python — Effective Python Testing with pytest.
| Jest (JS) | pytest (Python) | Notes |
|---|---|---|
npm test | pytest | Run all tests |
test("...", () => {}) | def test_something(): | Define a test |
expect(x).toBe(y) | assert x == y | Assertion |
expect(x).toEqual(y) | assert x == y | Deep equality |
expect(fn).toThrow() | with pytest.raises(Error): | Expect exception |
beforeEach(() => {}) | @pytest.fixture | Setup/teardown |
test.each([...]) | @pytest.mark.parametrize | Parameterized tests |
jest.fn() / jest.mock() | unittest.mock.MagicMock / monkeypatch | Mocking |
jest --coverage | pytest --cov | Coverage |
# math_utils.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def is_even(n):
return n % 2 == 0
# test_math_utils.py ← file must start with test_
import pytest
from math_utils import add, divide, is_even
# Function must start with test_
def test_add_two_positive_numbers():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, 1) == 0
def test_add_returns_int():
result = add(2, 3)
assert isinstance(result, int)
def test_divide_raises_on_zero():
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert "zero" in str(exc_info.value).lower()
def test_is_even():
assert is_even(4) is True
assert is_even(3) is False
# Run tests
pytest # discover and run all tests
pytest test_math_utils.py # specific file
pytest test_math_utils.py::test_add_two_positive_numbers # specific test
pytest -v # verbose output
pytest -x # stop on first failure
pytest -k "add" # run only tests matching "add"
pytest finds tests by scanning for files named test_*.py or *_test.py, and within them any function named test_* or class named Test*. No config needed to get started.
pytest rewrites assert statements to show detailed failure output — much better than bare assert.
# All pytest assertions use plain Python assert
assert result == expected
assert result != unexpected
assert result is None
assert result is not None
assert result is True
assert "hello" in result # membership
assert len(result) == 3
assert result > 0
assert abs(result - 3.14) < 0.01 # float comparison
# Approximate float comparison — use pytest.approx
assert result == pytest.approx(3.14159, abs=1e-3)
assert 0.1 + 0.2 == pytest.approx(0.3) # floating point safe
# Exception details
with pytest.raises(ValueError, match="zero"): # match against error message
divide(10, 0)
# Check exception type
with pytest.raises((TypeError, ValueError)):
risky_operation()
# Check it does NOT raise (just run it)
result = add(1, 2) # would fail the test if it raises
Fixtures are pytest's beforeEach — reusable setup that is injected into tests by name. They're more powerful than Jest's hooks because they're composable and can do teardown too.
# conftest.py — fixtures here are available to all test files
import pytest
@pytest.fixture
def sample_user():
"""A fresh user dict for each test that requests it."""
return {"name": "Alice", "age": 30, "email": "alice@example.com"}
@pytest.fixture
def db_connection():
"""Set up and tear down a database connection."""
conn = create_test_db()
yield conn # ← everything after yield is teardown
conn.close() # always runs, even if test fails
cleanup_test_db()
# Use fixtures by matching the parameter name
def test_user_name(sample_user):
assert sample_user["name"] == "Alice"
def test_user_age(sample_user):
assert sample_user["age"] == 30
def test_save_user(db_connection, sample_user):
db_connection.save(sample_user)
found = db_connection.find("Alice")
assert found["email"] == "alice@example.com"
# scope="function" (default) — fresh fixture per test
# scope="class" — shared within a test class
# scope="module" — shared across the whole file
# scope="session" — shared across all tests in the run
@pytest.fixture(scope="session")
def expensive_resource():
"""Created once, shared across all tests — e.g. a database."""
resource = create_expensive_thing()
yield resource
resource.destroy()
Run the same test with multiple inputs — like Jest's test.each.
test.each([
[2, 3, 5],
[-1, 1, 0],
[0, 0, 0],
])("add(%i, %i) = %i", (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(100, -100, 0),
])
def test_add(a, b, expected):
assert add(a, b) == expected
# Runs 4 separate tests — all shown in output
# Parametrize with pytest.param for custom IDs and marks
@pytest.mark.parametrize("n, expected", [
pytest.param(4, True, id="even-positive"),
pytest.param(3, False, id="odd-positive"),
pytest.param(0, True, id="zero"),
pytest.param(-2, True, id="even-negative"),
])
def test_is_even(n, expected):
assert is_even(n) == expected
Python's unittest.mock is your jest.fn() and jest.mock().
# The code under test
import requests
def get_github_user(username):
resp = requests.get(f"https://api.github.com/users/{username}")
resp.raise_for_status()
return resp.json()["name"]
# test_github.py
from unittest.mock import patch, MagicMock
import pytest
from github import get_github_user
# patch — replace requests.get with a mock for this test only
def test_get_github_user(monkeypatch):
mock_response = MagicMock()
mock_response.json.return_value = {"name": "Linus Torvalds"}
mock_response.raise_for_status.return_value = None
monkeypatch.setattr("github.requests.get", lambda url: mock_response)
result = get_github_user("torvalds")
assert result == "Linus Torvalds"
# Alternatively use @patch decorator
@patch("github.requests.get")
def test_get_github_user_patch(mock_get):
mock_get.return_value.json.return_value = {"name": "Linus Torvalds"}
mock_get.return_value.raise_for_status.return_value = None
assert get_github_user("torvalds") == "Linus Torvalds"
mock_get.assert_called_once_with("https://api.github.com/users/torvalds")
# MagicMock directly
def test_with_mock():
mock_fn = MagicMock(return_value=42)
result = mock_fn(1, 2)
assert result == 42
mock_fn.assert_called_once_with(1, 2)
# Skip a test
@pytest.mark.skip(reason="not implemented yet")
def test_future_feature():
...
# Skip conditionally
import sys
@pytest.mark.skipif(sys.platform == "win32", reason="Linux only")
def test_linux_specific():
...
# Mark expected failure
@pytest.mark.xfail(reason="known bug #123")
def test_known_bug():
assert broken_function() == 42
# Custom marks (register in pyproject.toml)
@pytest.mark.slow
def test_heavy_computation():
...
# Run only slow tests: pytest -m slow
# Run all except slow: pytest -m "not slow"
# Install
pip install pytest-cov
# Run with coverage
pytest --cov=my_package --cov-report=term-missing
# HTML report
pytest --cov=my_package --cov-report=html
# opens htmlcov/index.html
# Example output:
# Name Stmts Miss Cover
# ------------------------------------
# math_utils.py 10 2 80%
# pyproject.toml config
[tool.coverage.run]
source = ["my_package"]
omit = ["*/tests/*", "*/migrations/*"]
[tool.coverage.report]
fail_under = 80 # fail if coverage drops below 80%
my_project/
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── math_utils.py
│ └── api.py
├── tests/
│ ├── conftest.py ← shared fixtures
│ ├── test_math_utils.py
│ └── test_api.py
├── pyproject.toml
└── requirements.txt
# pyproject.toml — configure pytest
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"
1. How does pytest discover test functions automatically?
2. What does yield inside a @pytest.fixture do?
3. To test that a function raises a ValueError, you use:
4. @pytest.mark.parametrize("x, y", [(1,2),(3,4)]) on a test function will run it…
5. What is conftest.py used for in a pytest project?
Questions answered correctly.
You now have a solid Python foundation as a JavaScript developer.
Next step: build something real.