Skip to content

    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:

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

    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
    

    Now the tests. The pytest.raises checks that an exception occurs, and the parametrize runs the same test with various data:

    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

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

    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"
    

    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:

    Terminal
    pytest -q
    
    Output
    ..............                                                           [100%]
    14 passed in 0.01s