Pular para o conteúdo

    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:

    Terminal
    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:

    intermediario/cap40_pytest.pylinhas 10 a 25
    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:

    intermediario/cap40_pytest.pylinhas 27 a 46
    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):

    intermediario/cap40_pytest.pylinhas 51 a 68
    @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"
    
    intermediario/cap40_pytest.pylinhas 70 a 84
    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:

    Terminal
    pytest -q
    
    Saída
    ..............                                                           [100%]
    14 passed in 0.01s