Skip to lesson
Python for JS Developers

Async & TestingLesson 2 of 2

Testing with pytest

3 min

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

JavaScriptPythonNote
test('adds', () => {})def test_adds():Plain functions
describe('Calc', () => {})class TestCalc: (or one file)Grouping is optional
expect(x).toBe(y)assert x == yPlain assert
expect(() => f()).toThrow()with pytest.raises(ValueError):Context manager
beforeEach(() => ...)@pytest.fixtureInjected by parameter name
test.each([[1, 2]])@pytest.mark.parametrizeOne test per case
jest.mock('./module')@patch('module.fn')unittest.mock

Side by side

JavaScript
// 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);
    }
  );
});
Python
# 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 browser

Write pytest tests for parse_email (it is already defined for you):

Starter code
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