Chapter 40, Intermediate
Testing with pytest
Code without tests is code you are afraid to touch. Pytest has the lowest barrier to entry among Python testing tools.
Code for this chapter: intermediario/cap40_pytest.py
Install
Pytest is a development dependency. Install it in the project environment:
uv add --dev pytest
Without uv, inside a virtual environment: python -m pip install pytest.
The first test
A test is a function whose name starts with test_, and the body uses the assert common in Python. When the assert fails, pytest shows the involved values. First, the code we are going to test:
import pytest
def eh_primo(n: int) -> bool:
if n < 2:
return False
for divisor in range(2, int(n ** 0.5) + 1):
if n % divisor == 0:
return False
return True
def dividir(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("divisor não pode ser zero")
return a / b
Now the tests. The pytest.raises checks that an exception occurs, and the parametrize runs the same test with various data:
def test_primo_simples():
assert eh_primo(7)
assert not eh_primo(8)
def test_dividir():
assert dividir(10, 4) == 2.5
def test_dividir_por_zero():
with pytest.raises(ZeroDivisionError, match="zero"):
dividir(1, 0)
@pytest.mark.parametrize(
"numero, esperado",
[(0, False), (1, False), (2, True), (17, True), (18, False)],
)
def test_eh_primo_parametrizado(numero, esperado):
assert eh_primo(numero) is esperado
Fixtures
A fixture prepares something that the test needs and is provided by the parameter name. Pytest comes with several ready: tmp_path (a temporary folder), capsys (captures what was printed) and monkeypatch (changes environment variables and attributes, and reverts at the end):
@pytest.fixture
def carrinho():
return {"itens": [], "total": 0.0}
def test_carrinho_comeca_vazio(carrinho):
assert carrinho["itens"] == []
def test_tmp_path(tmp_path):
arquivo = tmp_path / "dados.txt"
arquivo.write_text("oi", encoding="utf-8")
assert arquivo.read_text(encoding="utf-8") == "oi"
def test_capsys(capsys):
print("olá")
assert capsys.readouterr().out == "olá\n"
import os
def ler_ambiente():
return os.environ.get("AMBIENTE", "desenvolvimento")
def test_ambiente_padrao(monkeypatch):
monkeypatch.delenv("AMBIENTE", raising=False)
assert ler_ambiente() == "desenvolvimento"
def test_ambiente_producao(monkeypatch):
monkeypatch.setenv("AMBIENTE", "producao")
assert ler_ambiente() == "producao"
What to test
I follow a priority order: first the happy path, then the boundaries (zero, empty, the largest value), then the expected errors. For each bug fixed, I write the test that reproduces it first, so it never comes back. And test behavior, not implementation: if you change the algorithm and the tests break without the result changing, they are testing the wrong thing.
Exercise 1
Test a conversion with tolerance
Write celsius_para_fahrenheit(c) and a test that uses pytest.approx for the decimal value 36.6, which becomes 97.88.
Running the tests
Point pytest to the file, or to the folder tests/ of your project. The option -q keeps the output short:
pytest -q
.............. [100%]
14 passed in 0.01s