Você já se perguntou como os grandes projetos de software conseguem manter milhares de linhas de código funcionando sem erros? A resposta está nos testes unitários. Neste guia completo, você vai aprender como fazer testes unitários no Python e transformar a qualidade do seu código.
O Que São Testes Unitários e Por Que São Importantes
Testes unitários são pequenos pedaços de código que verificam se partes específicas do seu programa funcionam corretamente. Imagine que você está construindo um carro. Antes de testar o veículo completo, você testa cada componente separadamente: o motor, os freios, as rodas. É exatamente isso que os testes unitários fazem com o seu código.
Quando você escreve uma função em Python, ela pode ter dezenas de comportamentos diferentes dependendo dos dados que recebe. Testar manualmente cada cenário toda vez que você faz uma mudança no código é demorado e sujeito a erros. Com testes unitários, você automatiza esse processo.
Para entender melhor como funcionam os testes unitários no Python, confira este vídeo tutorial do canal Muri Tech que explica os conceitos fundamentais de forma clara e prática:
Os benefícios dos testes unitários vão muito além de encontrar bugs. Eles ajudam você a escrever código mais organizado, facilitam a manutenção futura e dão confiança para fazer mudanças sem medo de quebrar algo que já estava funcionando. Empresas como Google, Facebook e Netflix investem pesadamente em testes automatizados justamente por esses motivos.
Conhecendo as Principais Bibliotecas de Teste
Python oferece várias ferramentas para criar testes unitários. As duas mais populares são unittest e pytest. Vamos entender as diferenças entre elas e quando usar cada uma.
Unittest: A Biblioteca Nativa
O unittest vem instalado automaticamente quando você faz a instalação do Python. Isso significa que você pode começar a escrever testes imediatamente, sem precisar instalar nada extra. A biblioteca foi inspirada no JUnit, uma ferramenta popular para testes em Java.
O unittest usa uma abordagem baseada em classes. Você cria uma classe que herda de unittest.TestCase e define métodos de teste dentro dela. Cada método de teste deve começar com a palavra test_ para que o framework reconheça automaticamente.
import unittest
def somar(a, b):
return a + b
class TestCalculadora(unittest.TestCase):
def test_somar_positivos(self):
resultado = somar(2, 3)
self.assertEqual(resultado, 5)
def test_somar_negativos(self):
resultado = somar(-1, -1)
self.assertEqual(resultado, -2)
if __name__ == '__main__':
unittest.main()O código acima mostra um teste básico com unittest. Criamos uma função simples de soma e testamos dois cenários diferentes. O método assertEqual verifica se o resultado obtido é igual ao resultado esperado.
Pytest: Simplicidade e Poder
O pytest é a biblioteca de testes mais popular da comunidade Python. Ela é conhecida por sua sintaxe simples e recursos avançados. Para usar o pytest, você precisa instalá-lo primeiro através do pip.
pip install pytestA grande vantagem do pytest é que você não precisa criar classes. Basta escrever funções normais e usar a palavra assert do Python. Veja como fica o mesmo teste anterior usando pytest:
def somar(a, b):
return a + b
def test_somar_positivos():
assert somar(2, 3) == 5
def test_somar_negativos():
assert somar(-1, -1) == -2Muito mais simples, não é? O pytest encontra automaticamente todos os arquivos que começam com test_ e executa as funções de teste. Você pode rodar os testes com um comando simples no terminal:
pytestPrimeiros Passos: Criando Seu Primeiro Teste
Vamos criar um exemplo prático completo do zero. Imagine que você está desenvolvendo um sistema de gerenciamento de tarefas e precisa testar uma função que calcula quantos dias faltam para o prazo de uma tarefa.
Primeiro, crie um arquivo chamado tarefas.py com a função que queremos testar:
from datetime import datetime, timedelta
def dias_ate_prazo(data_prazo):
"""
Calcula quantos dias faltam até o prazo.
Args:
data_prazo: String no formato 'YYYY-MM-DD'
Returns:
Número de dias (int)
"""
hoje = datetime.now().date()
prazo = datetime.strptime(data_prazo, '%Y-%m-%d').date()
diferenca = prazo - hoje
return diferenca.daysAgora crie um arquivo de testes chamado test_tarefas.py. É importante que o nome comece com test_ para que o pytest encontre automaticamente:
from datetime import datetime, timedelta
from tarefas import dias_ate_prazo
def test_prazo_futuro():
"""Testa quando o prazo está no futuro"""
amanha = datetime.now() + timedelta(days=1)
data_str = amanha.strftime('%Y-%m-%d')
assert dias_ate_prazo(data_str) == 1
def test_prazo_hoje():
"""Testa quando o prazo é hoje"""
hoje = datetime.now().strftime('%Y-%m-%d')
assert dias_ate_prazo(hoje) == 0
def test_prazo_passado():
"""Testa quando o prazo já passou"""
ontem = datetime.now() - timedelta(days=1)
data_str = ontem.strftime('%Y-%m-%d')
assert dias_ate_prazo(data_str) == -1Execute os testes com o comando pytest no terminal. Você verá uma saída mostrando quantos testes passaram e se algum falhou. O pytest usa cores para facilitar a visualização: verde para testes que passaram, vermelho para os que falharam.
Estruturando Seus Testes de Forma Profissional
Conforme seu projeto cresce, organizar os testes se torna fundamental. Uma boa estrutura ajuda você e sua equipe a encontrar e manter os testes facilmente. Veja uma organização recomendada para projetos Python:
meu_projeto/
├── src/
│ ├── __init__.py
│ ├── calculadora.py
│ ├── validador.py
│ └── processador.py
├── tests/
│ ├── __init__.py
│ ├── test_calculadora.py
│ ├── test_validador.py
│ └── test_processador.py
├── pytest.ini
└── requirements.txtSepare seu código principal na pasta src/ e todos os testes na pasta tests/. Para cada arquivo de código, crie um arquivo de teste correspondente. Se você tem calculadora.py, crie test_calculadora.py. Essa convenção facilita muito encontrar os testes relacionados a cada parte do código.
O arquivo pytest.ini na raiz do projeto permite configurar como o pytest se comporta. Aqui está um exemplo de configuração útil:
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=shortEssas configurações dizem ao pytest para procurar testes na pasta tests/, mostrar informações detalhadas durante a execução (-v) e exibir mensagens de erro resumidas (--tb=short).
Métodos Assert Mais Utilizados
Os métodos assert são o coração dos testes unitários. Eles verificam se o comportamento do código está correto. No unittest, você tem vários métodos disponíveis. Vamos conhecer os mais importantes:
| Método | O Que Verifica | Exemplo |
|---|---|---|
| assertEqual(a, b) | Se a é igual a b | self.assertEqual(2+2, 4) |
| assertNotEqual(a, b) | Se a é diferente de b | self.assertNotEqual(1, 2) |
| assertTrue(x) | Se x é verdadeiro | self.assertTrue(5 > 3) |
| assertFalse(x) | Se x é falso | self.assertFalse(2 > 5) |
| assertIn(a, b) | Se a está em b | self.assertIn(‘a’, ‘casa’) |
| assertIsNone(x) | Se x é None | self.assertIsNone(variavel) |
| assertRaises(erro) | Se uma exceção é lançada | self.assertRaises(ValueError) |
Veja um exemplo prático usando diferentes métodos assert com unittest:
import unittest
class TestValidador(unittest.TestCase):
def test_validar_email(self):
email = "usuario@exemplo.com"
self.assertIn('@', email)
self.assertTrue(email.endswith('.com'))
def test_lista_vazia(self):
lista = []
self.assertEqual(len(lista), 0)
self.assertFalse(lista)
def test_divisao_por_zero(self):
with self.assertRaises(ZeroDivisionError):
resultado = 10 / 0No pytest, você pode usar o assert simples do Python para a maioria dos casos. O pytest é inteligente o suficiente para mostrar informações detalhadas quando um teste falha, mesmo com asserts simples. Para verificar exceções no pytest, use pytest.raises:
import pytest
def test_divisao_por_zero():
with pytest.raises(ZeroDivisionError):
resultado = 10 / 0Testando Diferentes Cenários com Parametrização
Muitas vezes você precisa testar a mesma função com vários conjuntos de dados diferentes. Em vez de escrever um teste separado para cada caso, você pode usar parametrização. Isso torna seus testes mais limpos e fáceis de manter.
O pytest oferece o decorator @pytest.mark.parametrize para isso. Veja um exemplo testando uma função de validação de senha com múltiplos casos:
import pytest
def senha_forte(senha):
"""Verifica se a senha tem pelo menos 8 caracteres"""
return len(senha) >= 8
@pytest.mark.parametrize("senha,esperado", [
("abc123", False),
("senha123", True),
("12345678", True),
("curta", False),
("senhasupersegura", True),
])
def test_validacao_senha(senha, esperado):
assert senha_forte(senha) == esperadoEsse único teste será executado cinco vezes, uma para cada combinação de senha e resultado esperado. Se algum caso falhar, o pytest mostra exatamente qual foi. Isso é muito mais eficiente do que escrever cinco funções de teste separadas.
Você também pode parametrizar múltiplos argumentos. Veja um exemplo com uma função de cálculo de desconto:
@pytest.mark.parametrize("preco,percentual,esperado", [
(100, 10, 90),
(50, 20, 40),
(200, 0, 200),
(150, 50, 75),
])
def test_calcular_desconto(preco, percentual, esperado):
resultado = aplicar_desconto(preco, percentual)
assert resultado == esperadoFixtures: Preparando o Ambiente de Teste
Fixtures são recursos que seus testes precisam para funcionar. Podem ser conexões com banco de dados, arquivos temporários, objetos complexos ou qualquer coisa que precise ser configurada antes dos testes rodarem. O pytest tem um sistema poderoso de fixtures.
Imagine que você está testando uma classe que gerencia uma lista de compras. Em vez de criar essa lista em cada teste, você cria uma fixture:
import pytest
class ListaCompras:
def __init__(self):
self.itens = []
def adicionar(self, item):
self.itens.append(item)
def remover(self, item):
self.itens.remove(item)
def total_itens(self):
return len(self.itens)
@pytest.fixture
def lista_compras():
"""Cria uma lista de compras para os testes"""
return ListaCompras()
def test_adicionar_item(lista_compras):
lista_compras.adicionar("Arroz")
assert lista_compras.total_itens() == 1
assert "Arroz" in lista_compras.itens
def test_remover_item(lista_compras):
lista_compras.adicionar("Feijão")
lista_compras.adicionar("Arroz")
lista_compras.remover("Feijão")
assert lista_compras.total_itens() == 1A fixture lista_compras é executada antes de cada teste que a solicita. Note que você simplesmente adiciona o nome da fixture como parâmetro na função de teste. O pytest cuida de tudo automaticamente.
Fixtures podem ter diferentes escopos. Por padrão, uma nova fixture é criada para cada teste. Mas você pode definir que uma fixture seja criada apenas uma vez por módulo ou por sessão:
@pytest.fixture(scope="module")
def conexao_banco():
"""Conexão compartilhada entre todos os testes do módulo"""
conexao = criar_conexao()
yield conexao
conexao.fechar()O comando yield divide a fixture em duas partes: o que vem antes é a configuração, o que vem depois é a limpeza. Isso garante que recursos sejam liberados corretamente após os testes.
Mocks: Simulando Comportamentos Externos
Nem sempre você pode testar algo que depende de recursos externos. Por exemplo, você não quer que seus testes enviem emails reais ou façam chamadas para APIs pagas toda vez que rodam. É aí que entram os mocks.
Um mock é um objeto falso que simula o comportamento de algo real. Python tem a biblioteca unittest.mock para isso, que funciona tanto com unittest quanto com pytest. Veja um exemplo testando uma função que busca dados de uma API:
from unittest.mock import Mock, patch
import requests
def buscar_usuario(user_id):
"""Busca dados de um usuário na API"""
resposta = requests.get(f'https://api.exemplo.com/users/{user_id}')
return resposta.json()
def test_buscar_usuario():
# Cria um mock da resposta da API
mock_resposta = Mock()
mock_resposta.json.return_value = {
'id': 1,
'nome': 'João Silva',
'email': 'joao@exemplo.com'
}
# Substitui requests.get pelo mock
with patch('requests.get', return_value=mock_resposta):
usuario = buscar_usuario(1)
assert usuario['nome'] == 'João Silva'
assert usuario['email'] == 'joao@exemplo.com'O teste acima não faz nenhuma chamada real à API. O patch substitui temporariamente requests.get por um mock que retorna os dados que definimos. Isso torna o teste rápido, confiável e independente de conexão com internet.
Você também pode verificar se uma função foi chamada e com quais argumentos:
def test_enviar_email():
with patch('smtplib.SMTP') as mock_smtp:
enviar_email('usuario@exemplo.com', 'Assunto', 'Corpo')
# Verifica se o email foi enviado
mock_smtp.assert_called_once()
# Verifica os argumentos da chamada
args, kwargs = mock_smtp.call_args
assert 'usuario@exemplo.com' in str(args)Medindo a Cobertura dos Testes
Cobertura de testes mostra qual porcentagem do seu código é executada pelos testes. Uma cobertura alta indica que você está testando bem seu código, mas não garante que os testes sejam bons. O ideal é ter pelo menos 80% de cobertura.
Para medir a cobertura no pytest, instale o plugin pytest-cov:
pip install pytest-covExecute os testes com o relatório de cobertura:
pytest --cov=src tests/Você verá um relatório mostrando a porcentagem de cobertura de cada arquivo. Para um relatório mais detalhado em HTML, use:
pytest --cov=src --cov-report=html tests/Isso cria uma pasta htmlcov/ com arquivos HTML. Abra htmlcov/index.html no navegador e você verá exatamente quais linhas do seu código não estão sendo testadas. As linhas verdes foram executadas, as vermelhas não foram.
Você pode adicionar a configuração de cobertura no pytest.ini:
[pytest]
addopts = --cov=src --cov-report=html --cov-report=term-missing
testpaths = testsCom essa configuração, toda vez que você rodar pytest, o relatório de cobertura será gerado automaticamente. A opção --cov-report=term-missing mostra no terminal quais linhas não foram cobertas.
Boas Práticas em Testes Unitários
Seguir boas práticas faz a diferença entre testes que ajudam e testes que atrapalham. Um teste bem escrito é fácil de entender, rápido de executar e confiável nos resultados. Aqui estão as práticas mais importantes:
Teste apenas uma coisa por vez. Cada teste deve verificar um comportamento específico. Se um teste falha, você deve saber imediatamente qual parte do código tem problema. Evite testes que verificam múltiplas coisas não relacionadas.
Use nomes descritivos. O nome do teste deve dizer exatamente o que está sendo testado. Em vez de test_funcao1, use test_calcular_desconto_retorna_preco_reduzido. Quando um teste falhar, você entenderá o problema apenas lendo o nome.
Mantenha testes independentes. Um teste nunca deve depender de outro. A ordem de execução não deve importar. Cada teste deve configurar tudo que precisa e limpar depois, de preferência usando fixtures.
Testes devem ser rápidos. Testes unitários devem rodar em milissegundos. Se estão demorando segundos, provavelmente você está testando muita coisa junta ou fazendo operações pesadas. Use mocks para simular operações lentas como banco de dados ou requisições de rede.
Escreva testes antes ou junto com o código. A prática de TDD (Test-Driven Development) sugere escrever o teste antes da funcionalidade. Isso força você a pensar no design da função antes de implementá-la. Se você não pratica TDD completo, pelo menos escreva testes logo após implementar cada função.
Teste casos extremos. Não teste apenas o caminho feliz. Teste o que acontece com valores negativos, zero, strings vazias, None, listas vazias. Esses casos extremos são onde a maioria dos bugs aparece.
def test_casos_extremos():
# Testa valores limites
assert calcular_idade(0) == 0
assert calcular_idade(150) raises ValueError
# Testa tipos inesperados
with pytest.raises(TypeError):
calcular_idade("abc")
# Testa valores None
with pytest.raises(ValueError):
calcular_idade(None)Integrando Testes no Fluxo de Desenvolvimento
Testes só são úteis se você os executa regularmente. A melhor prática é integrar os testes no seu fluxo de trabalho usando ferramentas de integração contínua (CI/CD). Sempre que alguém faz um commit no repositório, os testes rodam automaticamente.
Serviços como GitHub Actions, GitLab CI e Travis CI executam seus testes toda vez que você envia código. Veja um exemplo de configuração do GitHub Actions que roda testes em Python:
name: Testes
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Configurar Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Instalar dependências
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Executar testes
run: pytest --cov=src tests/Salve esse arquivo como .github/workflows/tests.yml no seu repositório. Agora os testes rodam automaticamente a cada push ou pull request. Se algum teste falhar, o GitHub bloqueia o merge até você corrigir.
Durante o desenvolvimento local, configure seu editor para mostrar resultados dos testes. A maioria das IDEs modernas como VS Code e PyCharm tem plugins que executam testes automaticamente quando você salva um arquivo.
Erros Comuns ao Escrever Testes
Mesmo desenvolvedores experientes cometem alguns erros clássicos ao escrever testes. Conhecer esses problemas ajuda você a evitá-los desde o início.
Testar implementação em vez de comportamento. Seus testes devem verificar o que a função faz, não como ela faz. Se você mudar a implementação interna mas o comportamento continua o mesmo, os testes não devem quebrar.
Testes muito acoplados ao código. Se você precisa mudar três testes toda vez que muda uma linha de código, seus testes estão acoplados demais. Use abstrações e fixtures para reduzir esse acoplamento.
Ignorar testes que falham. Se um teste está falhando intermitentemente (às vezes passa, às vezes falha), não ignore. Esses “flaky tests” geralmente indicam problemas reais no código, como condições de corrida ou dependências de estado global.
Testar código de terceiros. Não escreva testes para bibliotecas que você usa. Se você está usando requests ou pandas, assuma que eles já foram testados. Teste apenas seu próprio código.
Testes muito grandes. Se um teste tem 50 linhas de código, provavelmente está testando coisa demais. Divida em testes menores. Cada teste deve ter no máximo 10-15 linhas, incluindo configuração e verificação.
Conclusão
Testes unitários são essenciais para desenvolver software de qualidade em Python. Eles permitem que você desenvolva com confiança, refatore código sem medo e identifique problemas rapidamente. Começar com testes simples usando unittest ou pytest é o primeiro passo.
A jornada com testes é contínua. Comece testando as partes mais críticas do seu código. Use parametrização para cobrir múltiplos cenários. Aproveite fixtures para organizar a configuração dos testes. E sempre meça a cobertura para saber onde adicionar mais testes.
Lembre-se: o objetivo não é ter 100% de cobertura, mas ter testes que realmente agregam valor. Um bom conjunto de testes é aquele que dá confiança para fazer mudanças e te avisa quando algo quebra. Continue praticando e seus testes vão melhorar naturalmente com o tempo.
Perguntas Frequentes (FAQ)
1. Qual a diferença entre unittest e pytest?
Unittest vem instalado com Python e usa classes, enquanto pytest precisa instalação mas tem sintaxe mais simples com funções.
2. Preciso testar 100% do código?
Não. Busque pelo menos 80% de cobertura focando nas partes críticas. Qualidade dos testes importa mais que quantidade.
3. Como testar funções que acessam banco de dados?
Use mocks para simular a conexão com banco ou crie um banco de teste temporário que é apagado após os testes.
4. Posso usar unittest e pytest juntos?
Sim, pytest executa testes escritos com unittest. Você pode migrar gradualmente para pytest mantendo testes antigos.
5. Quando devo escrever os testes?
O ideal é antes ou logo após implementar a função. Nunca deixe para depois, pois testes retroativos são mais difíceis.
6. Como testar funções com entrada do usuário?
Use mock para simular o input. Substitua a função input() por um mock que retorna valores predefinidos para cada teste.
7. O que são testes de integração?
Testes que verificam se múltiplas partes do sistema funcionam juntas. Diferentes de testes unitários que testam funções isoladas.
8. Como acelerar testes lentos?
Use mocks para operações externas, execute testes em paralelo com pytest-xdist, e use fixtures com escopo adequado.
9. Preciso testar funções privadas?
Geralmente não. Teste apenas a API pública. Funções privadas são testadas indiretamente através das funções públicas.
10. Como organizar testes em projetos grandes?
Espelhe a estrutura do código fonte na pasta de testes. Use conftest.py para compartilhar fixtures entre módulos.





