Testing with pytest
Write and run tests, readable assertion failures, parametrize, fixtures, mocking and organising a test suite.
Professional code comes with automated tests: small programs that check your code does what it should. Tests let you refactor without fear, catch bugs before users do, and document how code is meant to be used. In Python, the de facto standard is pytest — tests are plain functions with plain assert statements, and pytest takes care of discovery, reporting and much more. Every Python job description that mentions testing means pytest.
Your first test#
Say you have a small module:
Create a test file next to it. Names matter: files must be called test_*.py (or *_test.py), and test functions must start with test:
Run pytest from the project folder:
Each . is a passing test. Use pytest -v for one line per test, pytest -q for quiet output, and pytest test_pricing.py::test_total_of_items to run a single test.
When a test fails#
pytest's killer feature is its assertion introspection: you write a plain assert, and on failure it shows exactly what the values were. Suppose a bug sneaks into apply_discount (it forgets to round):
No special assertEqual methods to remember — just assert. (Notice the floating-point noise in 84.99149999999999, too — exactly the kind of thing tests catch.)
Comparing floats
Floating-point results rarely match exactly. Use pytest.approx:
Parametrize: many cases, one test#
Instead of copy-pasting tests for different inputs, use @pytest.mark.parametrize:
Each case runs and reports separately, so one failing input doesn't hide the others.
Fixtures: reusable setup#
Tests often need the same setup: a sample object, a temporary file, a database connection. A fixture is a function decorated with @pytest.fixture; any test that names it as a parameter receives its return value:
Every test gets a fresh cart, so tests can't interfere with each other. A fixture that uses yield runs its teardown code after the test, even if the test fails. Put fixtures shared by many test files in a conftest.py file — pytest loads it automatically.
Useful built-in fixtures: tmp_path (a temporary directory), monkeypatch (temporarily replace attributes, environment variables or functions), capsys (capture printed output), and caplog (capture logs).
Mocking external dependencies#
Unit tests should be fast and deterministic — no real network calls, no real emails. Replace those dependencies with fakes using monkeypatch (or unittest.mock):
Patch the name where it's looked up (weather.requests.get, or weather.get_temperature), not where it was originally defined.
Organising a test suite#
A typical project layout:
Configure pytest in pyproject.toml:
More handy tools:
- Markers:
@pytest.mark.skip(reason=...),@pytest.mark.skipif(sys.platform == "win32", reason=...),@pytest.mark.xfailfor known bugs, and custom markers like@pytest.mark.slow(run with-m "not slow"). - Coverage:
pip install pytest-cov, thenpytest --cov=shopshows which lines your tests never execute. - Useful flags:
-x(stop at first failure),--lf(rerun last failures),-k discount(run tests whose names match),--pdb(drop into the debugger on failure). - CI: run
pyteston every push with GitHub Actions or GitLab CI.
What makes a good test#
- One behaviour per test, with a descriptive name:
test_rejects_zero_qtytells you what broke. - Arrange, Act, Assert: set up, do the thing, check the result.
- Test behaviour, not implementation details, so refactoring doesn't break tests needlessly.
- Cover edge cases: empty input, zero, negative numbers, very large values, invalid types, unicode.
- Keep tests fast — mock the network, use
tmp_pathinstead of real folders, use in-memory SQLite. - Consider writing the test first (test-driven development): it forces you to design the interface before the implementation.
Common mistakes#
- Misnamed files or functions (
pricing_tests.py,def check_total()) — pytest silently skips them. - Tests that depend on each other or on execution order — use fixtures for fresh state.
- Comparing floats with
==— usepytest.approx. - Hitting real APIs or production databases in unit tests.
- Catching the exception yourself in a test instead of using
pytest.raises.
What's next#
Tests tell you that something is broken; next you'll learn tools for finding out why: logging and debugging.
Check your understanding
Quick quiz
1.How does pytest find your tests by default?
2.How do you assert that a function raises
ValueErrorin pytest?3.What is a pytest fixture?
Finished reading?
Mark this lesson complete to track your progress.