Async & TestingLesson 2 of 2
Testing with pytest
If you know Jest or Vitest, pytest will feel refreshingly simple. There is no expect().toBe() chain: you write functions whose names start with test_ and use the plain assert statement. When an assertion fails, pytest rewrites it to show you both sides of the comparison.
You do not need describe() blocks. pytest groups tests by file (test_*.py) and, optionally, by class (class TestCalculator: with test_ methods, no inheritance needed). Run everything with pytest, or filter by name with -k.
Fixtures replace beforeEach/afterEach. Define a function decorated with @pytest.fixture, and any test that declares a parameter with the same name receives the fixture's return value. It is dependency injection by parameter name. For teardown, write the fixture as a generator: code after the yield runs after the test, like afterEach.
Parametrize replaces test.each(). The @pytest.mark.parametrize decorator runs the same test with different inputs, and pytest reports each case as a separate test.
For mocking, the standard library's unittest.mock provides patch() and MagicMock (the equivalent of jest.mock and jest.fn), and pytest's built-in monkeypatch fixture is handy for environment variables and attributes.
Quick reference
| JavaScript | Python | Note |
|---|---|---|
| test('adds', () => {}) | def test_adds(): | Plain functions |
| describe('Calc', () => {}) | class TestCalc: (or one file) | Grouping is optional |
| expect(x).toBe(y) | assert x == y | Plain assert |
| expect(() => f()).toThrow() | with pytest.raises(ValueError): | Context manager |
| beforeEach(() => ...) | @pytest.fixture | Injected by parameter name |
| test.each([[1, 2]]) | @pytest.mark.parametrize | One test per case |
| jest.mock('./module') | @patch('module.fn') | unittest.mock |
Side by side
// Jest
describe('Calculator', () => {
let calc;
beforeEach(() => { calc = new Calculator(); });
test('adds', () => {
expect(calc.add(2, 3)).toBe(5);
});
test('throws', () => {
expect(() => calc.add('a', 1)).toThrow();
});
test.each([[1, 2, 3], [4, 5, 9]])(
'add(%i, %i) = %i', (a, b, exp) => {
expect(calc.add(a, b)).toBe(exp);
}
);
});# test_calculator.py
import pytest
from calculator import Calculator
@pytest.fixture
def calc(): # like beforeEach
return Calculator()
def test_add(calc): # fixture injected by name
assert calc.add(2, 3) == 5
def test_invalid(calc):
with pytest.raises(TypeError):
calc.add("a", 1)
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(4, 5, 9),
(-1, 1, 0),
])
def test_add_cases(calc, a, b, expected):
assert calc.add(a, b) == expected
from unittest.mock import patch
@patch("myapp.api.fetch_user")
def test_mock(mock_fetch):
mock_fetch.return_value = {"name": "Alice"}
assert get_user_name(1) == "Alice"Exercise
Real Python, running in your browserWrite pytest tests for parse_email (it is already defined for you):
def parse_email(email):
if "@" not in email:
raise ValueError("Missing @")
parts = email.split("@")
if len(parts) != 2 or not parts[0] or not parts[1]:
raise ValueError("Invalid format")
return (parts[0], parts[1])
Write:
1. A fixture that returns a valid email address
2. A test that uses the fixture and checks the returned tuple
3. A test that checks a missing @ raises ValueError
4. A parametrized test with 3+ valid cases
Your tests are run with real pytest.Ctrl + Enter runCtrl + Shift + Enter checkTab indentEsc then Tab to leave the editorCtrl + / comment