pytest na prática
Nesta aula, você vai aprender a usar o pytest na prática, cobrindo asserções, fixtures, parametrização e organização de testes. Vamos explorar desde a sintaxe básica de asserções até boas práticas de estruturação de projetos, com exemplos prontos para aplicar no seu dia a dia.
O pytest é um dos frameworks de teste mais populares da linguagem Python, conhecido por sua simplicidade, flexibilidade e poderosos recursos. Nesta aula, vamos colocar a mão na massa: você vai aprender a escrever testes eficientes usando asserções, criar fixtures reutilizáveis, parametrizar casos de teste e organizar seus testes de forma profissional. Ao final, você terá as ferramentas necessárias para incorporar o pytest no seu fluxo de desenvolvimento com confiança.
Vamos começar com uma visão geral do pytest e depois mergulhar em cada tópico com exemplos práticos. Se você já conhece o básico de testes unitários, esta aula vai te dar um passo além, mostrando como o pytest simplifica tarefas que em outros frameworks seriam mais complexas.
Asserções
No pytest, as asserções são feitas com a palavra-chave assert do Python, o que elimina a necessidade de métodos especiais como assertEqual ou assertTrue de outros frameworks. Isso torna o código de teste mais limpo e legível, pois usa a sintaxe nativa da linguagem.
Quando uma asserção falha, o pytest exibe uma mensagem detalhada mostrando os valores envolvidos, o que facilita a depuração. Além disso, o pytest possui um mecanismo de introspecção que mostra exatamente onde a comparação falhou, mesmo em expressões complexas.
# test_exemplo.py
def test_soma():
assert 1 + 1 == 2
def test_string_contem():
nome = "Python"
assert "yth" in nome
def test_lista_ordenada():
numeros = [3, 1, 2]
assert numeros == sorted(numeros)
Você também pode adicionar uma mensagem personalizada após a asserção, separada por vírgula, para fornecer contexto adicional quando a falha ocorrer.
def test_valor_esperado():
resultado = calcular_juros(1000, 0.05)
assert resultado == 1050, f"Esperado 1050, mas obteve {resultado}"
O pytest também permite usar a função pytest.raises para verificar se uma exceção específica é lançada, o que é essencial para testar tratamentos de erro.
import pytest
def dividir(a, b):
if b == 0:
raise ValueError("Divisão por zero")
return a / b
def test_divisao_por_zero():
with pytest.raises(ValueError):
dividir(10, 0)
Fixtures
Fixtures são funções que fornecem dados ou recursos para os testes, promovendo a reutilização e a separação de configuração. No pytest, você define uma fixture com o decorador @pytest.fixture e a utiliza como parâmetro da função de teste.
Um dos grandes benefícios das fixtures é o controle de escopo: você pode definir se a fixture será executada uma vez por teste, uma vez por módulo, uma vez por sessão, etc. Isso ajuda a otimizar a execução quando há recursos caros, como conexões de banco de dados.
import pytest
@pytest.fixture
def usuario():
# Configuração: cria um usuário de teste
u = {"nome": "Ana", "email": "ana@example.com"}
return u
def test_email_valido(usuario):
assert "@" in usuario["email"]
def test_nome_nao_vazio(usuario):
assert usuario["nome"] != ""
Fixtures também podem ser compostas, ou seja, uma fixture pode depender de outra. Basta declarar a dependência como parâmetro da fixture.
@pytest.fixture
def banco():
# Simula conexão com banco
return {"conectado": True}
@pytest.fixture
def usuario_com_banco(banco, usuario):
# Combina dados do usuário com banco
usuario["banco"] = banco
return usuario
def test_usuario_tem_banco(usuario_com_banco):
assert usuario_com_banco["banco"]["conectado"]
Para lidar com limpeza, você pode usar a palavra-chave yield dentro da fixture, separando a configuração da desmontagem. O código após o yield será executado após o teste, garantindo que os recursos sejam liberados.
@pytest.fixture
def arquivo_temporario(tmp_path):
arquivo = tmp_path / "dados.txt"
arquivo.write_text("conteúdo")
yield arquivo
# cleanup: apaga o arquivo (opcional, pois tmp_path é limpo automaticamente)
Parametrização
A parametrização permite executar o mesmo teste com diferentes conjuntos de dados, evitando duplicação de código. No pytest, usamos o decorador @pytest.mark.parametrize para passar múltiplos argumentos a uma função de teste.
Isso é extremamente útil para testar funções com várias entradas e saídas esperadas, aumentando a cobertura sem escrever vários testes quase idênticos.
import pytest
def soma(a, b):
return a + b
@pytest.mark.parametrize("a,b,esperado", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_soma_parametrizado(a, b, esperado):
assert soma(a, b) == esperado
Você também pode parametrizar múltiplos argumentos de uma só vez e até combinar com fixtures, tornando seus testes muito mais flexíveis.
@pytest.mark.parametrize("entrada", ["texto", "outra coisa", ""])
def test_tamanho(entrada):
assert len(entrada) >= 0
Quando um teste parametrizado falha, o pytest identifica qual combinação de parâmetros causou a falha, exibindo os valores na mensagem de erro. Isso facilita a correção rápida.
Organização
Organizar os testes é fundamental para a manutenibilidade do projeto. O pytest segue uma convenção de descoberta de testes: ele procura por arquivos nomeados test_*.py ou *_test.py e funções que começam com test_. Você pode organizar seus testes em diretórios e módulos de forma lógica, refletindo a estrutura do código fonte.
Uma boa prática é colocar os testes em um diretório separado, como tests/, e usar arquivos de configuração como pytest.ini ou pyproject.toml para definir opções de execução, como caminhos de busca e marcadores.
# Estrutura de diretórios sugerida:
# meu_projeto/
# ├── meu_modulo.py
# ├── tests/
# │ ├── __init__.py
# │ ├── test_meu_modulo.py
# │ └── conftest.py
O arquivo conftest.py é especial: ele permite definir fixtures e hooks que estarão disponíveis para todos os testes do diretório e subdiretórios. É o lugar ideal para fixtures globais.
# tests/conftest.py
import pytest
@pytest.fixture
def dados_globais():
return {"api_url": "https://api.example.com"}
Além disso, você pode usar marcadores para categorizar testes e executá-los seletivamente. Por exemplo, marcar testes como lentos ou de integração.
import pytest
@pytest.mark.slow
def test_processamento_pesado():
# teste demorado
pass
Para executar apenas os testes com um marcador específico, use pytest -m slow. Você precisa registrar os marcadores no arquivo de configuração para evitar avisos.
# pytest.ini
[pytest]
markers =
slow: testes lentos
Boas Práticas e Observações Finais
Escrever testes com pytest é uma habilidade essencial para qualquer desenvolvedor Python. Algumas boas práticas incluem: manter os testes independentes entre si, usar fixtures para configurações comuns, nomear os testes de forma descritiva e rodar os testes com frequência durante o desenvolvimento. Lembre-se de que testes não são apenas para verificar se o código funciona, mas também para documentar o comportamento esperado e prevenir regressões.
Uma dica final: explore a documentação oficial e a comunidade do pytest, que é rica em exemplos e plugins úteis. Comece com testes simples e vá evoluindo conforme a complexidade do seu projeto.
Referências
- Documentação oficial do pytest
- Como usar asserções no pytest
- Fixtures no pytest
- Parametrização no pytest
- Uso e organização de testes
- Documentação do unittest (para comparação)
Exercícios
- Crie uma função que receba uma lista de números e retorne a soma dos quadrados. Escreva testes usando asserções para verificar o resultado com pelo menos três entradas diferentes.
- Crie uma fixture que forneça uma lista de números de 1 a 10. Use essa fixture em dois testes: um que verifica se a soma é 55 e outro que verifica se o comprimento é 10.
- Escreva uma função que valide se uma string é um email (contém pelo menos um '@' e um '.'). Parametrize o teste com pelo menos 4 casos: dois válidos e dois inválidos.
- Crie uma estrutura de diretórios para um projeto chamado
meu_projetocom um módulocalculadora.pye testes emtests/test_calculadora.py. Escreva um teste para a funçãomultiplicar. - Utilize o marcador
@pytest.mark.slowpara marcar um teste que simule uma operação demorada (por exemplo,time.sleep(0.1)). Escreva o teste e a configuração necessária nopytest.inipara registrar o marcador.
def soma_quadrados(numeros):
return sum(x**2 for x in numeros)
# test_soma_quadrados.py
def test_soma_quadrados():
assert soma_quadrados([1, 2, 3]) == 14
assert soma_quadrados([0, 0, 0]) == 0
assert soma_quadrados([-1, -2]) == 5
import pytest
@pytest.fixture
def numeros():
return list(range(1, 11))
def test_soma(numeros):
assert sum(numeros) == 55
def test_tamanho(numeros):
assert len(numeros) == 10
def email_valido(email):
return "@" in email and "." in email
import pytest
@pytest.mark.parametrize("email,esperado", [
("user@example.com", True),
("nome.sobrenome@dominio.org", True),
("usuario", False),
("@semdominio", False),
])
def test_email_valido(email, esperado):
assert email_valido(email) == esperado
meu_projeto/
├── calculadora.py
└── tests/
├── __init__.py
└── test_calculadora.py
calculadora.py:
def multiplicar(a, b):
return a * b
test_calculadora.py: from calculadora import multiplicar
def test_multiplicar():
assert multiplicar(3, 4) == 12
import time
import pytest
@pytest.mark.slow
def test_operacao_demorada():
time.sleep(0.1)
assert True
pytest.ini: [pytest]
markers =
slow: testes lentos