Capítulo 40, Intermediário
Testes com pytest
Código sem teste é código que você tem medo de mexer. O pytest tem a menor barreira de entrada entre as ferramentas de teste de Python.
Código deste capítulo: intermediario/cap40_pytest.py
Instalar
O pytest é uma dependência de desenvolvimento. Instale no ambiente do projeto:
uv add --dev pytest
Sem o uv, dentro de um ambiente virtual: python -m pip install pytest.
O primeiro teste
Um teste é uma função cujo nome começa com test_, e o corpo usa o assert comum do Python. Quando o assert falha, o pytest mostra os valores envolvidos. Primeiro o código que vamos testar:
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
Agora os testes. O pytest.raises verifica que uma exceção acontece, e o parametrize roda o mesmo teste com vários dados:
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
Uma fixture prepara algo de que o teste precisa e é entregue pelo nome do parâmetro. O pytest traz várias prontas: tmp_path (uma pasta temporária), capsys (captura o que foi impresso) e monkeypatch (altera variáveis de ambiente e atributos, e desfaz no fim):
@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"
O que testar
Eu sigo uma ordem de prioridade: primeiro o caminho feliz, depois os limites (zero, vazio, o maior valor), depois os erros esperados. Para cada bug corrigido, escrevo antes o teste que o reproduz, para ele nunca voltar. E teste comportamento, não implementação: se você trocar o algoritmo e os testes quebrarem sem que o resultado mude, eles estão testando a coisa errada.
Exercício 1
Teste uma conversão com tolerância
Escreva celsius_para_fahrenheit(c) e um teste que use pytest.approx para o valor decimal 36.6, que vira 97.88.
Rodar os testes
Aponte o pytest para o arquivo, ou para a pasta tests/ do seu projeto. A opção -q deixa a saída curta:
pytest -q
.............. [100%]
14 passed in 0.01s