pytest na prática
Nesta aula, você 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é padrões avançados de reutilização de código com fixtures e testes parametrizados, sempre com exemplos prontos para rodar.
O pytest é um dos frameworks de teste mais populares da linguagem Python, conhecido por sua simplicidade e poderosos recursos. Nesta aula, vamos mergulhar em quatro pilares essenciais do pytest: asserções, fixtures, parametrização e organização de testes. Você verá como escrever testes claros e eficientes, como reutilizar código de preparação e limpeza, como testar múltiplos cenários com pouco código e como estruturar projetos de teste de forma profissional.
Se você já conhece o básico de testes com pytest, esta aula vai aprofundar seu conhecimento e apresentar boas práticas. Se está começando, não se preocupe: cada conceito será explicado do zero, com exemplos práticos que você pode rodar no seu ambiente.
Asserções
Em pytest, as asserções são feitas com a palavra-chave assert do Python, mas o pytest aprimora a saída de erro, mostrando detalhes sobre os valores comparados. Isso torna a depuração muito mais fácil. Por exemplo, se você compara duas listas, o pytest mostra exatamente onde elas diferem.
Além das asserções simples, o pytest oferece um módulo pytest.raises para verificar exceções e pytest.approx para comparações de ponto flutuante. Vamos ver cada um com exemplos.
# test_assertions.py
import pytest
def test_igualdade():
assert 1 + 1 == 2
def test_pertence():
assert 'a' in 'abc'
def test_excecao():
with pytest.raises(ValueError):
int('abc')
def test_aprox():
assert 0.1 + 0.2 == pytest.approx(0.3)
Quando um teste falha, o pytest mostra uma mensagem detalhada. Por exemplo, no teste test_igualdade, se alterarmos para 1 + 1 == 3, a saída indicará que 2 não é igual a 3. Isso é muito mais informativo do que um simples False.
Fixtures
Fixtures são funções que fornecem dados ou recursos para os testes, evitando repetição e facilitando a limpeza. Elas são definidas com o decorador @pytest.fixture e podem ser injetadas nos testes como parâmetros. O pytest gerencia o ciclo de vida: a fixture é executada antes do teste e, se definida com yield, o código após o yield roda após o teste (teardown).
Um exemplo clássico é uma conexão com banco de dados ou um arquivo temporário. Vamos criar uma fixture que fornece uma lista de usuários.
# test_fixtures.py
import pytest
@pytest.fixture
usuario():
return {'nome': 'Ana', 'idade': 30}
def test_maioridade(usuario):
assert usuario['idade'] >= 18
def test_nome(usuario):
assert 'Ana' in usuario['nome']
Também podemos definir escopos de fixture: function (padrão), class, module, session. Isso controla com que frequência a fixture é criada. Por exemplo, uma fixture de module é criada uma vez por módulo de teste, economizando tempo em operações caras.
@pytest.fixture(scope='module')
def dados():
print('\nPreparando dados...')
yield [1, 2, 3]
print('\nLimpando...')
As fixtures também podem depender de outras fixtures, criando uma cadeia de preparação. Isso é poderoso para testes de integração.
Parametrização
A parametrização permite rodar o mesmo teste com diferentes entradas e saídas esperadas, usando o decorador @pytest.mark.parametrize. Isso evita duplicação de código e facilita a adição de novos casos.
Você pode passar uma lista de tuplas ou uma lista de dicionários. O pytest gera um teste para cada combinação, com IDs automáticos que podem ser personalizados.
# test_parametrize.py
import pytest
@pytest.mark.parametrize('entrada, esperado', [
(2, 4),
(3, 9),
(4, 16),
])
def test_quadrado(entrada, esperado):
assert entrada ** 2 == esperado
@pytest.mark.parametrize('texto, resultado', [
('', False),
('abc', True),
('123', False),
])
def test_tem_letra(texto, resultado):
assert any(c.isalpha() for c in texto) == resultado
Você também pode combinar múltiplas parametrizações, gerando o produto cartesiano dos argumentos. Para casos mais complexos, use IDs para identificar cada teste na saída.
@pytest.mark.parametrize('x', [1, 2], ids=['um', 'dois'])
@pytest.mark.parametrize('y', [10, 20], ids=['dez', 'vinte'])
def test_soma(x, y):
assert x + y > 0
Organização
Organizar testes é crucial para a manutenção do projeto. O pytest segue convenções: arquivos de teste devem começar com test_ ou terminar com _test.py, e funções de teste devem começar com test_. Normalmente, cria-se um diretório tests/ na raiz do projeto, com subpastas para cada módulo ou funcionalidade.
Além de organização de arquivos, é importante usar conftest.py para compartilhar fixtures entre vários arquivos de teste. O conftest.py é carregado automaticamente pelo pytest e pode conter fixtures, hooks e plugins.
# Estrutura de diretórios sugerida
projeto/
├── src/
│ └── meu_modulo.py
├── tests/
│ ├── conftest.py
│ ├── test_operacoes.py
│ └── test_utils/
│ ├── __init__.py
│ └── test_helpers.py
└── pyproject.toml
No conftest.py, você pode definir fixtures globais:
# tests/conftest.py
import pytest
@pytest.fixture
def cliente():
return {'nome': 'Teste', 'email': 'teste@exemplo.com'}
Outra prática é usar marcadores para categorizar testes, como @pytest.mark.slow para testes demorados. Você pode executar apenas testes com um marcador específico usando -m.
pytest -m slow
Por fim, use a opção -k para filtrar por nome e --maxfail para parar após N falhas. Essas ferramentas ajudam a gerenciar grandes suítes de teste.
Boas Práticas e Observações Finais
Algumas boas práticas ao usar pytest: mantenha os testes independentes e isolados, não dependa de ordem de execução; use fixtures para preparação e limpeza; nomeie os testes de forma descritiva; e evite testes que testam implementação em vez de comportamento.
Além disso, aproveite os plugins do ecossistema, como pytest-cov para cobertura de código, pytest-mock para mocking e pytest-django para projetos Django. Esses plugins ampliam o poder do pytest e tornam o desenvolvimento mais produtivo.
Exercícios
- Escreva uma função
par_ou_impar(n)que retorna 'par' ou 'ímpar'. Crie testes parametrizados para os valores 1, 2, 3, 4 e 5. - Crie uma fixture chamada
lista_palavrasque retorna uma lista com ['python', 'pytest', 'programação']. Use-a em dois testes: um que verifica o tamanho da lista e outro que verifica se 'pytest' está na lista. - Escreva um teste que verifica se a função
divide(a, b)levantaZeroDivisionErrorquandobé zero. Usepytest.raises. - Utilize a parametrização para testar uma função
soma_lista(lista)que retorna a soma dos elementos. Teste com listas vazias, listas de positivos, listas de negativos e listas mistas. - Crie um
conftest.pycom uma fixtureusuario_adminque retorna um dicionário com 'nome': 'Admin' e 'admin': True. Escreva um teste em um arquivo separado que use essa fixture e verifique se o usuário é admin.
import pytest
def par_ou_impar(n):
return 'par' if n % 2 == 0 else 'ímpar'
@pytest.mark.parametrize('n, esperado', [
(1, 'ímpar'),
(2, 'par'),
(3, 'ímpar'),
(4, 'par'),
(5, 'ímpar'),
])
def test_par_ou_impar(n, esperado):
assert par_ou_impar(n) == esperado
import pytest
@pytest.fixture
def lista_palavras():
return ['python', 'pytest', 'programação']
def test_tamanho(lista_palavras):
assert len(lista_palavras) == 3
def test_contem_pytest(lista_palavras):
assert 'pytest' in lista_palavras
import pytest
def divide(a, b):
return a / b
def test_divide_por_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
import pytest
def soma_lista(lista):
return sum(lista)
@pytest.mark.parametrize('lista, esperado', [
([], 0),
([1, 2, 3], 6),
([-1, -2, -3], -6),
([1, -2, 3], 2),
])
def test_soma_lista(lista, esperado):
assert soma_lista(lista) == esperado
import pytest
@pytest.fixture
def usuario_admin():
return {'nome': 'Admin', 'admin': True}
test_admin.py:def test_usuario_admin(usuario_admin):
assert usuario_admin['admin'] is True