Testes com pytest: do primeiro teste à cobertura de código

Cansado de programar?

Conheça a melhor e mais completa formação de Python e Django e sinta-se um programador verdadeiramente competente. Além de Python e Django, você também vai aprender Banco de Dados, SQL, HTML, CSS, Javascript, Bootstrap e muito mais!

Quero aprender Python e Django de Verdade! Quero aprender!
Suporte

Tire suas dúvidas diretamente com o professor

Projetos práticos

Projetos práticos voltados para o mercado de trabalho

Prática profissional

Formação moderna com foco na prática profissional

Resposta rápida

O pytest é o framework de testes mais usado em Python. Você cria um arquivo test_algo.py, escreve funções que começam com test_, verifica o resultado com assert e roda pytest no terminal. Ele encontra os testes sozinho e mostra o valor exato que falhou.

1
2
3
4
5
6
# test_soma.py
def somar(a, b):
    return a + b

def test_somar():
    assert somar(2, 3) == 5  # rode: pytest  ->  1 passed

Resumo em 30 segundos:

  • Instale com pip install pytest (versão 9 exige Python 3.10+).
  • Arquivos test_*.py e funções test_* são coletados automaticamente.
  • Fixtures (@pytest.fixture) preparam dados; @pytest.mark.parametrize roda o mesmo teste com várias entradas.
  • pytest.raises testa exceções; tmp_path dá uma pasta temporária para testar arquivos.
  • pytest arquivo.py::test_nome roda um teste só; pytest --cov mede cobertura (plugin pytest-cov).

Salve salve Pythonista!

Você escreve uma função, testa no terminal com meia dúzia de print(), tudo funciona. Uma semana depois muda uma linha e quebra algo que funcionava, sem perceber. Testes automatizados resolvem exatamente isso: eles conferem o seu código toda vez que você quiser, em segundos.

Neste guia você vai aprender a testar código Python com o pytest: do primeiro teste até fixtures, parametrização, testes de exceção, arquivos temporários e cobertura de código. Todos os exemplos foram executados com pytest 9.1.1, pytest-cov 7.1.0 e Python 3.13, e as saídas mostradas são as reais (só o caminho da pasta foi encurtado).

Então… Bora pro post! :rocket:

Vá Direto ao Assunto…

O que é o pytest e por que usar

O que é o pytest? O pytest é uma biblioteca de testes para Python que descobre e executa automaticamente funções de teste. Em vez de métodos especiais como assertEqual(), você usa o assert nativo da linguagem, e o pytest reescreve essas asserções para mostrar, em caso de falha, cada valor envolvido na comparação.

Um teste nada mais é do que um código que chama o seu código e confere o resultado. A biblioteca padrão do Python já traz o módulo unittest, mas o pytest virou o padrão da comunidade por três motivos:

  • Menos cerimônia: testes são funções simples, sem classes obrigatórias.
  • Mensagens de falha claras: ele mostra o valor calculado, o valor esperado e a diferença.
  • Recursos poderosos: fixtures, parametrização, marcadores e centenas de plugins (como o pytest-cov).

Se você ainda não domina a criação de funções, vale revisar o nosso guia de funções em Python antes de seguir, porque todo teste do pytest é uma função.

Instalando o pytest

Crie e ative um ambiente virtual (se precisar de ajuda, veja o post sobre virtualenv e ambientes virtuais) e instale:

1
2
pip install pytest
pytest --version
1
pytest 9.1.1

O pytest 9 deixou de suportar Python 3.9, então você precisa de Python 3.10 ou mais novo. A documentação oficial fica em docs.pytest.org.

Seu primeiro teste com pytest

Vamos testar um módulo simples, calculadora.py, salvo em uma pasta de estudo (aqui, primeiro_teste/):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def somar(a, b):
    return a + b


def dividir(a, b):
    if b == 0:
        raise ValueError("Não é possível dividir por zero")
    return a / b


def aplicar_desconto(preco, percentual):
    if not 0 <= percentual <= 100:
        raise ValueError("O percentual deve estar entre 0 e 100")
    return round(preco * (1 - percentual / 100), 2)

Agora o arquivo de testes, test_calculadora.py, na mesma pasta. Repare nas duas convenções que o pytest usa para achar os testes: o arquivo começa com test_ e cada função de teste também.

1
2
3
4
5
6
7
8
9
from calculadora import somar


def test_somar_dois_inteiros():
    assert somar(2, 3) == 5


def test_somar_negativos():
    assert somar(-1, -1) == -2

Rode no terminal, na pasta do projeto:

1
pytest
1
2
3
4
5
6
7
8
============================= test session starts ==============================
platform linux -- Python 3.13.12, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/voce/primeiro_teste
collected 2 items

test_calculadora.py ..                                                   [100%]

============================== 2 passed in 0.00s ===============================

Cada ponto (.) é um teste que passou. Uma falha aparece como F e um erro na preparação do teste como E. Com pytest -v (verbose) você vê o nome de cada teste:

1
2
test_calculadora.py::test_somar_dois_inteiros PASSED                     [ 50%]
test_calculadora.py::test_somar_negativos PASSED                         [100%]

Um bom teste segue o padrão preparar, agir, verificar (em inglês, Arrange, Act, Assert): monte os dados, chame a função e confira o resultado com assert. Cada teste deve verificar um comportamento e ter um nome que diga qual é.

O assert e a introspecção de falhas

O assert é uma instrução do próprio Python: se a expressão for verdadeira, nada acontece; se for falsa, ele levanta AssertionError. O que o pytest acrescenta é a introspecção: ele reescreve os assert dos arquivos de teste para mostrar os valores intermediários quando algo falha.

Imagine que alguém “simplificou” a função de desconto e introduziu um bug:

1
2
def aplicar_desconto(preco, percentual):
    return round(preco * percentual / 100, 2)  # bug: calcula o desconto, não o preço final

O teste que confere 10% de desconto em R$ 200:

1
2
3
4
5
from calculadora import aplicar_desconto


def test_desconto_de_10_por_cento():
    assert aplicar_desconto(200, 10) == 180

A saída real do pytest:

1
2
3
4
5
6
7
8
9
10
11
12
=================================== FAILURES ===================================
________________________ test_desconto_de_10_por_cento _________________________

    def test_desconto_de_10_por_cento():
>       assert aplicar_desconto(200, 10) == 180
E       assert 20.0 == 180
E        +  where 20.0 = aplicar_desconto(200, 10)

test_calculadora.py:5: AssertionError
=========================== short test summary info ============================
FAILED test_calculadora.py::test_desconto_de_10_por_cento - assert 20.0 == 180
============================== 1 failed in 0.02s ===============================

Leia de baixo para cima: a linha com > é o assert que falhou, as linhas com E mostram que a função devolveu 20.0 quando se esperava 180, e o where diz de onde veio o 20.0. Você descobre o bug sem colocar um único print().

Com coleções, a introspecção fica ainda mais útil. Comparando dicionários, rode com -vv para ver o diff completo:

1
2
3
4
5
6
7
>       assert usuario == {"nome": "Ana", "idade": 31, "cidade": "Recife"}
E       AssertionError: assert {'nome': 'Ana', 'idade': 30, 'cidade': 'Recife'} == {'nome': 'Ana', 'idade': 31, 'cidade': 'Recife'}
E         
E         Common items:
E         {'cidade': 'Recife', 'nome': 'Ana'}
E         Differing items:
E         {'idade': 30} != {'idade': 31}

Em listas, ele aponta o índice: At index 2 diff: 18.0 != 19.

Atenção com números decimais. 0.1 + 0.2 não é exatamente 0.3 em ponto flutuante. Para comparar float, use pytest.approx:

1
2
3
4
5
import pytest


def test_soma_float_approx():
    assert 0.1 + 0.2 == pytest.approx(0.3)

Estrutura de pastas e configuração

Para projetos reais, separe o código dos testes. Uma estrutura simples e que funciona bem:

1
2
3
4
5
6
7
8
9
10
11
12
meu_projeto/
├── pyproject.toml
├── loja/
│   ├── __init__.py
│   ├── calculadora.py
│   ├── carrinho.py
│   └── relatorio.py
└── tests/
    ├── conftest.py
    ├── test_calculadora.py
    ├── test_carrinho.py
    └── test_relatorio.py

Os testes importam o código pelo nome do pacote (from loja.calculadora import somar). Para o pytest achar o pacote loja a partir da pasta tests/, configure o pyproject.toml:

1
2
3
[tool.pytest]
testpaths = ["tests"]
pythonpath = ["."]

A tabela [tool.pytest] com tipos nativos do TOML é novidade do pytest 9. Em versões anteriores (ou se você preferir), a forma clássica [tool.pytest.ini_options] continua funcionando, com as mesmas chaves. Só não use as duas tabelas ao mesmo tempo. Detalhes na referência de configuração do pytest.

  • testpaths diz onde procurar testes quando você roda só pytest.
  • pythonpath adiciona a raiz do projeto ao caminho de importação.

Em projetos que viram pacotes instaláveis, a documentação do pytest recomenda o src layout (código em src/loja/) com o pacote instalado em modo editável (pip install -e .). Para aprender e para projetos pequenos, a estrutura acima é suficiente.

Está curtindo esse conteúdo? :thumbsup:

Que tal receber 30 dias de conteúdo direto na sua Caixa de Entrada?

Sua assinatura não pôde ser validada.
Você fez sua assinatura com sucesso.

Assine as PyDicas e receba 30 dias do melhor conteúdo Python na sua Caixa de Entrada: direto e sem enrolação!

Fixtures: preparando dados para os testes

Muitos testes precisam do mesmo ponto de partida: um objeto montado, uma lista de dados, uma conexão. Copiar essa preparação em cada teste deixa tudo repetitivo. Fixtures resolvem isso.

Veja uma classe simples de carrinho de compras (se classes ainda são novidade, confira o post sobre classes e objetos no Python):

1
2
3
4
5
6
7
8
9
10
11
12
# loja/carrinho.py
class Carrinho:
    def __init__(self):
        self.itens = []

    def adicionar(self, nome, preco, quantidade=1):
        if quantidade < 1:
            raise ValueError("A quantidade deve ser pelo menos 1")
        self.itens.append({"nome": nome, "preco": preco, "quantidade": quantidade})

    def total(self):
        return sum(item["preco"] * item["quantidade"] for item in self.itens)

A fixture fica no tests/conftest.py, arquivo especial que o pytest carrega sozinho. Tudo o que é definido nele vale para todos os testes da pasta, sem precisar importar:

1
2
3
4
5
6
7
8
9
10
11
12
# tests/conftest.py
import pytest

from loja.carrinho import Carrinho


@pytest.fixture
def carrinho_com_itens():
    carrinho = Carrinho()
    carrinho.adicionar("Caderno", 25.0, quantidade=2)
    carrinho.adicionar("Caneta", 3.5)
    return carrinho

Para usar, basta declarar um parâmetro com o mesmo nome da fixture:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# tests/test_carrinho.py
from loja.carrinho import Carrinho


def test_carrinho_novo_esta_vazio():
    assert Carrinho().total() == 0


def test_total(carrinho_com_itens):
    assert carrinho_com_itens.total() == 53.5


def test_adicionar_mais_um_item(carrinho_com_itens):
    carrinho_com_itens.adicionar("Mochila", 120.0)
    assert len(carrinho_com_itens.itens) == 3

Um detalhe importante: por padrão, a fixture é recriada para cada teste. O item adicionado em test_adicionar_mais_um_item não vaza para outros testes. Isso mantém os testes independentes, e a ordem de execução não muda o resultado.

Fixtures com yield (preparação e limpeza)

Quando o recurso precisa ser fechado depois do teste, use yield no lugar de return. O que vem antes do yield é a preparação; o que vem depois roda ao final do teste, mesmo que ele falhe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import sqlite3

import pytest


@pytest.fixture
def conexao():
    con = sqlite3.connect(":memory:")
    con.execute("CREATE TABLE alunos (nome TEXT)")
    print("\n[setup] banco criado")
    yield con
    con.close()
    print("[teardown] banco fechado")


def test_inserir_aluno(conexao):
    conexao.execute("INSERT INTO alunos VALUES ('Ana')")
    total = conexao.execute("SELECT COUNT(*) FROM alunos").fetchone()[0]
    assert total == 1

Rodando com pytest -s -q (o -s mostra os print()):

1
2
3
4
[setup] banco criado
.[teardown] banco fechado

1 passed in 0.00s

Parametrize: o mesmo teste com várias entradas

Testar só um valor é pouco. Para cobrir vários casos sem copiar e colar o teste, use @pytest.mark.parametrize. O primeiro argumento é uma string com os nomes dos parâmetros; o segundo, uma lista de tuplas com os valores:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pytest

from loja.calculadora import aplicar_desconto


@pytest.mark.parametrize(
    "preco, percentual, esperado",
    [
        (100, 10, 90.0),
        (200, 25, 150.0),
        (59.9, 0, 59.9),
        (80, 100, 0.0),
    ],
)
def test_aplicar_desconto(preco, percentual, esperado):
    assert aplicar_desconto(preco, percentual) == esperado

O pytest gera um teste para cada tupla, com um identificador entre colchetes:

1
2
3
4
tests/test_calculadora.py::test_aplicar_desconto[100-10-90.0] PASSED     [ 25%]
tests/test_calculadora.py::test_aplicar_desconto[200-25-150.0] PASSED    [ 50%]
tests/test_calculadora.py::test_aplicar_desconto[59.9-0-59.9] PASSED     [ 75%]
tests/test_calculadora.py::test_aplicar_desconto[80-100-0.0] PASSED      [100%]

Se um caso falhar, os outros continuam rodando e você vê exatamente qual combinação quebrou. Inclua sempre os casos de borda: zero, 100%, lista vazia, texto vazio, números negativos.

pytest.raises: testando exceções

Tão importante quanto testar o caminho feliz é testar se o código falha do jeito certo. Se a função deve levantar ValueError com percentual inválido, teste isso com pytest.raises (para revisar como exceções funcionam, veja o guia de tratamento de erros e exceções no Python):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pytest

from loja.calculadora import aplicar_desconto, dividir


@pytest.mark.parametrize("percentual", [-5, 101, 150])
def test_percentual_invalido(percentual):
    with pytest.raises(ValueError, match="entre 0 e 100"):
        aplicar_desconto(100, percentual)


def test_dividir_por_zero():
    with pytest.raises(ValueError) as excinfo:
        dividir(10, 0)
    assert str(excinfo.value) == "Não é possível dividir por zero"
  • O teste passa se a exceção indicada for levantada dentro do bloco with.
  • O match recebe uma expressão regular procurada na mensagem da exceção (com re.search).
  • Com as excinfo você acessa a exceção em excinfo.value para verificações extras.

Se a exceção não acontecer, o teste falha com Failed: DID NOT RAISE. Veja a documentação de pytest.raises para mais opções.

tmp_path: testando código que lê e grava arquivos

Testes não devem sujar a sua pasta de projeto com arquivos de teste. A fixture embutida tmp_path entrega um objeto pathlib.Path apontando para uma pasta temporária única para cada teste:

1
2
3
4
5
6
# loja/relatorio.py
def salvar_relatorio(caminho, linhas):
    with open(caminho, "w", encoding="utf-8") as arquivo:
        for linha in linhas:
            arquivo.write(linha + "\n")
    return len(linhas)
1
2
3
4
5
6
7
8
9
10
11
# tests/test_relatorio.py
from loja.relatorio import salvar_relatorio


def test_salvar_relatorio(tmp_path):
    arquivo = tmp_path / "vendas.txt"

    quantidade = salvar_relatorio(arquivo, ["janeiro: 100", "fevereiro: 150"])

    assert quantidade == 2
    assert arquivo.read_text(encoding="utf-8") == "janeiro: 100\nfevereiro: 150\n"

Você não precisa criar nem apagar a pasta: o pytest cuida disso (ele mantém só as pastas das últimas execuções, para você inspecionar se quiser). Outras fixtures embutidas úteis são capsys (captura o que foi impresso com print()) e monkeypatch (troca variáveis de ambiente e atributos durante o teste).

Como rodar só um teste (e outras opções úteis)

Em projetos com centenas de testes, você não quer rodar tudo a cada alteração. As formas mais usadas de escolher testes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# um arquivo
pytest tests/test_carrinho.py

# uma função específica (arquivo::função)
pytest tests/test_carrinho.py::test_total

# um caso específico de um teste parametrizado
pytest "tests/test_calculadora.py::test_percentual_invalido[101]"

# todos os testes cujo nome contém "desconto"
pytest -k desconto

# expressões com and, or e not
pytest -k "carrinho and not zero"

Saída real do pytest -k desconto -v no nosso projeto:

1
2
3
4
5
6
tests/test_calculadora.py::test_aplicar_desconto[100-10-90.0] PASSED     [ 25%]
tests/test_calculadora.py::test_aplicar_desconto[200-25-150.0] PASSED    [ 50%]
tests/test_calculadora.py::test_aplicar_desconto[59.9-0-59.9] PASSED     [ 75%]
tests/test_calculadora.py::test_aplicar_desconto[80-100-0.0] PASSED      [100%]

======================= 4 passed, 11 deselected in 0.02s =======================

A tabela resume as opções de linha de comando que você mais vai usar:

Opção O que faz Quando usar
-v / -vv Mostra o nome de cada teste / diff completo Para ver o que rodou ou entender uma falha longa
-q Saída resumida Em suítes grandes, para uma visão rápida
-x Para na primeira falha Quando uma falha provoca várias outras
--lf Roda só os que falharam na última execução Enquanto corrige um bug
-k expr Filtra por nome do teste Para focar em uma funcionalidade
-s Mostra os print() dos testes Para depurar (o pytest captura a saída por padrão)
--cov=pacote Mede cobertura (requer pytest-cov) Para achar código sem teste

Medir cobertura só faz sentido quando existe um projeto de verdade por trás dos testes. É esse tipo de projeto, do básico ao Django, que você constrói na Jornada Python:

Cobertura de código com pytest-cov

Cobertura mede quais linhas do seu código foram executadas pelos testes. Ela não prova que o código está correto, mas mostra com clareza o que ninguém testou. Instale o plugin e rode:

1
2
pip install pytest-cov
pytest --cov=loja --cov-report=term-missing
1
2
3
4
5
6
7
8
9
10
11
_______________ coverage: platform linux, python 3.13.12-final-0 _______________

Name                  Stmts   Miss  Cover   Missing
---------------------------------------------------
loja/__init__.py          0      0   100%
loja/calculadora.py      10      1    90%   8
loja/carrinho.py          9      0   100%
loja/relatorio.py         5      0   100%
---------------------------------------------------
TOTAL                    24      1    96%
============================== 15 passed in 0.07s ==============================

A coluna Missing entrega o ouro: a linha 8 de calculadora.py nunca rodou. É o return a / b da função dividir. Nós testamos a divisão por zero, mas esquecemos de testar uma divisão normal! Um teste como assert dividir(10, 4) == 2.5 leva o arquivo a 100%.

Não persiga 100% a qualquer custo. Uma cobertura alta com asserts fracos (ou sem asserts) é enganosa. Use o relatório para achar caminhos importantes sem teste, como tratamentos de erro e condições de borda. Para gerar um relatório navegável em HTML, use --cov-report=html e abra a pasta htmlcov/. Mais opções na documentação do pytest-cov.

pytest ou unittest: qual usar?

Critério pytest unittest
Instalação pip install pytest Já vem com o Python
Forma do teste Funções simples Classes que herdam de TestCase
Verificação assert comum self.assertEqual(), self.assertTrue()…
Mensagem de falha Mostra valores e diff automaticamente Mais limitada
Dados compartilhados Fixtures reutilizáveis (conftest.py) setUp() e tearDown() por classe
Vários casos @pytest.mark.parametrize self.subTest() ou loops
Roda testes do outro? Sim, roda testes do unittest Não

Em projetos novos, o pytest é a escolha mais comum. O unittest faz sentido quando você não pode instalar dependências, ou em código legado que já o usa (e o pytest roda esses testes sem mudança).

Erros comuns

Estes são os erros que mais aparecem quando se começa com pytest, com a mensagem real do pytest 9.1.1.

Nome do arquivo ou da função fora do padrão (no tests ran)

1
2
3
# arquivo: verifica_soma.py
def verifica_soma():
    assert 1 + 1 == 2
1
2
3
collected 0 items

============================ no tests ran in 0.01s =============================

Correção: renomeie o arquivo para test_soma.py e a função para test_soma(). O pytest só coleta test_*.py (ou *_test.py) e funções que começam com test.

Módulo não encontrado ao importar o código (ModuleNotFoundError)

Acontece ao rodar pytest com o código em um pacote e os testes em tests/, sem configurar o pythonpath:

1
2
3
tests/conftest.py:3: in <module>
    from loja.carrinho import Carrinho
E   ModuleNotFoundError: No module named 'loja'

Correção: adicione pythonpath = ["."] na seção [tool.pytest] do pyproject.toml (como mostrado acima) ou rode python -m pytest, que inclui a pasta atual no caminho de importação.

Nome de fixture errado (fixture not found)

1
2
def test_total(carrinho_cheio):
    assert carrinho_cheio.total() == 53.5
1
E       fixture 'carrinho_cheio' not found

Correção: o parâmetro precisa ter exatamente o nome da função decorada com @pytest.fixture (aqui, carrinho_com_itens), e a fixture precisa estar no mesmo arquivo ou em um conftest.py acima do teste. Logo abaixo da mensagem, o pytest lista as fixtures disponíveis.

Esperar a exceção errada no pytest.raises (DID NOT RAISE)

1
2
3
4
5
6
7
8
9
10
import pytest


def dividir(a, b):
    return a / b


def test_nao_levanta():
    with pytest.raises(ZeroDivisionError):
        dividir(10, 2)
1
E       Failed: DID NOT RAISE ZeroDivisionError

Correção: dentro do with pytest.raises(...) coloque apenas a chamada que deve falhar, com os argumentos que provocam a falha (aqui, dividir(10, 0)). E confira o tipo: se a função levanta ZeroDivisionError e você espera ValueError, o erro original escapa e o teste falha.

Usar return em vez de assert (o teste passa sem testar nada)

1
2
def test_retorna():
    return 1 + 1 == 3
1
2
3
PytestReturnNotNoneWarning: Test functions should return None, but test_return.py::test_retorna returned <class 'bool'>.
  Did you mean to use `assert` instead of `return`?
========================= 1 passed, 1 warning in 0.01s =========================

Este é o mais traiçoeiro: a comparação é falsa e mesmo assim aparece 1 passed, só com um aviso. Correção: um teste verifica com assert, nunca com return. Leia sempre o resumo de warnings no fim da execução.

Exercícios resolvidos

Tente resolver cada exercício antes de abrir a solução. Todas as soluções foram executadas com pytest 9.1.1 e passaram. Para praticar a lógica por trás das funções testadas, use também a nossa página de exercícios de Python resolvidos.

Exercício 1. Escreva a função eh_par(n) e dois testes: um confirmando que 4 é par e outro que 7 não é.

Ver solução
1
2
3
4
5
6
7
8
9
10
def eh_par(n):
    return n % 2 == 0


def test_numero_par():
    assert eh_par(4)


def test_numero_impar():
    assert not eh_par(7)

Saída: 2 passed

assert eh_par(4) já basta, pois a função retorna um booleano. Escrever assert eh_par(4) == True funciona, mas é redundante.

Exercício 2. Usando @pytest.mark.parametrize, teste a função eh_palindromo(texto) (que ignora espaços e maiúsculas) com “arara”, “Ana”, “socorram me subi no onibus em marrocos” (todos True) e “python” (False).

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pytest


def eh_palindromo(texto):
    limpo = texto.replace(" ", "").lower()
    return limpo == limpo[::-1]


@pytest.mark.parametrize(
    "texto, esperado",
    [
        ("arara", True),
        ("Ana", True),
        ("socorram me subi no onibus em marrocos", True),
        ("python", False),
    ],
)
def test_eh_palindromo(texto, esperado):
    assert eh_palindromo(texto) == esperado

Saída: 4 passed

Um único teste parametrizado vira quatro testes independentes. Incluir um caso False é essencial: uma função que sempre devolvesse True passaria nos outros três.

Exercício 3. A função sacar(saldo, valor) devolve o novo saldo e levanta ValueError("Saldo insuficiente") se o valor for maior que o saldo. Escreva um teste para o saque normal e outro para o saque inválido.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pytest


def sacar(saldo, valor):
    if valor > saldo:
        raise ValueError("Saldo insuficiente")
    return saldo - valor


def test_saque_normal():
    assert sacar(100, 30) == 70


def test_saque_maior_que_saldo():
    with pytest.raises(ValueError, match="Saldo insuficiente"):
        sacar(100, 150)

Saída: 2 passed

O match garante que a exceção é a esperada, e não um ValueError qualquer vindo de outro ponto do código.

Exercício 4. Crie uma fixture notas que devolve {"Ana": 8.5, "Bruno": 6.0, "Carla": 9.5} e dois testes que a usem: um confere que há 3 alunos e outro que a maior nota é da Carla.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pytest


@pytest.fixture
def notas():
    return {"Ana": 8.5, "Bruno": 6.0, "Carla": 9.5}


def test_quantidade_de_alunos(notas):
    assert len(notas) == 3


def test_maior_nota(notas):
    assert max(notas, key=notas.get) == "Carla"

Saída: 2 passed

Os dois testes recebem o dicionário pelo parâmetro notas. Se amanhã os dados mudarem, você altera um lugar só.

Exercício 5. Teste a função contar_linhas(caminho), que conta as linhas de um arquivo de texto, sem criar arquivos na pasta do projeto.

Ver solução
1
2
3
4
5
6
7
8
9
def contar_linhas(caminho):
    with open(caminho, encoding="utf-8") as arquivo:
        return sum(1 for _ in arquivo)


def test_contar_linhas(tmp_path):
    arquivo = tmp_path / "dados.txt"
    arquivo.write_text("a\nb\nc\n", encoding="utf-8")
    assert contar_linhas(arquivo) == 3

Saída: 1 passed

A fixture tmp_path dá uma pasta temporária exclusiva do teste, e o write_text() do pathlib cria o arquivo de entrada.

Exercício 6. A média de [0.1, 0.2, 0.4] é 0.23333333333333336 em ponto flutuante. Escreva um teste que confira que a função media() devolve aproximadamente 0.2333, com tolerância de 0.001.

Ver solução
1
2
3
4
5
6
7
8
9
import pytest


def media(valores):
    return sum(valores) / len(valores)


def test_media():
    assert media([0.1, 0.2, 0.4]) == pytest.approx(0.2333, abs=1e-3)

Saída: 1 passed

Comparar float com == é frágil. O pytest.approx aceita uma tolerância relativa (rel) ou absoluta (abs); aqui usamos abs=1e-3.

Exercício 7 (estilo prova). Com a configuração padrão, qual das opções abaixo o pytest coleta e executa como teste?

a) Arquivo teste_login.py com a função test_login()
b) Arquivo test_login.py com a função verifica_login()
c) Arquivo test_login.py com a função test_login_valido()
d) Arquivo login.py com a função test_login()

Ver solução

Resposta: c.

Por padrão, o pytest coleta arquivos test_*.py ou *_test.py e, dentro deles, funções que começam com test. Em (a), teste_login.py não casa com test_*.py (o sublinhado precisa vir logo depois de test). Em (b), o arquivo está certo, mas a função não começa com test. Em (d), o arquivo não segue o padrão. Nos três casos o resultado é no tests ran.

Exercício 8 (estilo prova). Quantos testes o pytest executa para o código abaixo?

1
2
3
4
5
6
7
import pytest


@pytest.mark.parametrize("x", [1, 2, 3])
@pytest.mark.parametrize("y", [10, 20])
def test_combinacoes(x, y):
    assert x < y

a) 2
b) 3
c) 5
d) 6

Ver solução

Saída: 6 passed

Resposta: d. Decoradores parametrize empilhados geram o produto cartesiano dos valores: 3 valores de x vezes 2 valores de y dá 6 combinações. O pytest mostra os ids test_combinacoes[10-1], [10-2], [10-3], [20-1], [20-2] e [20-3].

Para ter código de verdade para testar, quer construir projetos reais, guiados do básico ao avançado? Conheça o nosso curso de Python completo.

Conclusão

Neste guia de testes com pytest, você aprendeu:

✅ Primeiro teste - arquivos test_*.py, funções test_* e o comando pytest
✅ assert e introspecção - como ler a saída de uma falha
✅ Fixtures - preparar dados com @pytest.fixture, conftest.py e yield
✅ Parametrize - um teste, vários casos
✅ pytest.raises e tmp_path - testar exceções e arquivos
✅ Cobertura - achar código sem teste com pytest-cov

Próximos passos:

  • Escreva testes para uma função que você já tem, começando pelos casos de borda
  • Rode pytest --cov e cubra as linhas que aparecem em Missing
  • Estude monkeypatch para isolar chamadas externas (APIs, data e hora)

Se ficou com alguma dúvida, fique à vontade para deixar um comentário no box aqui embaixo! Será um prazer te responder! :wink:

Perguntas frequentes

O que é o pytest?

O pytest é o framework de testes mais usado em Python. Você escreve funções cujo nome começa com test_, usa o assert comum da linguagem para verificar resultados e roda tudo com o comando pytest no terminal. Ele descobre os testes sozinho, mostra exatamente qual valor falhou e tem recursos como fixtures, parametrize e plugins.

Como instalar o pytest?

Dentro de um ambiente virtual ativado, rode pip install pytest. Para conferir a instalação, use pytest --version. A versão 9 do pytest exige Python 3.10 ou superior. Para medir cobertura de código, instale também o plugin com pip install pytest-cov.

Qual a diferença entre pytest e unittest?

O unittest vem na biblioteca padrão e exige classes que herdam de unittest.TestCase e métodos como self.assertEqual(). O pytest aceita funções simples com assert, tem fixtures mais flexíveis, parametrização nativa e mensagens de falha mais detalhadas. O pytest também consegue rodar testes escritos com unittest, então dá para migrar aos poucos.

Como rodar apenas um teste no pytest?

Passe o caminho do arquivo e o nome da função separados por dois-pontos duplos: pytest tests/test_carrinho.py::test_total. Para rodar todos os testes cujo nome contém uma palavra, use pytest -k desconto. Para reexecutar só os testes que falharam na última rodada, use pytest --lf.

O que é uma fixture no pytest?

Fixture é uma função marcada com @pytest.fixture que prepara dados ou recursos para os testes, como um objeto pronto, uma conexão de banco ou um arquivo. O teste recebe a fixture declarando um parâmetro com o mesmo nome da função. Fixtures colocadas no arquivo conftest.py ficam disponíveis para todos os testes da pasta.

Como testar se uma função levanta uma exceção no pytest?

Use o gerenciador de contexto pytest.raises: with pytest.raises(ValueError, match='texto'): seguido da chamada que deve falhar. O teste passa se a exceção for levantada e a mensagem casar com o match. Se nenhuma exceção acontecer, o pytest falha com Failed: DID NOT RAISE.

Qual o nome correto de um arquivo de teste para o pytest encontrar?

Por padrão o pytest coleta arquivos chamados test_*.py ou *_test.py e, dentro deles, funções que começam com test. Um arquivo verifica_soma.py ou uma função verifica_soma() são ignorados, e o pytest termina com no tests ran. Esses padrões podem ser alterados na configuração, mas o padrão é o recomendado.

Como ver a cobertura de testes com pytest?

Instale o plugin pytest-cov e rode pytest --cov=nome_do_pacote --cov-report=term-missing. O relatório mostra, por arquivo, quantas linhas existem, quantas não foram executadas pelos testes, o percentual de cobertura e, na coluna Missing, os números das linhas que nenhum teste alcançou.

Começe agora sua Jornada na Programação!

Não deixe para amanhã o sucesso que você pode começar a construir hoje!

#newsletter Olá :wave: Curtiu o artigo? Então faça parte da nossa Newsletter! Privacidade Não se preocupe, respeitamos sua privacidade. Você pode se descadastrar a qualquer momento.