Testes unitários são a base de qualquer software confiável. Em Python, o módulo unittest é a ferramenta nativa para criar e executar testes de forma estruturada e profissional. Se você deseja escrever código Python de qualidade, dominar o unittest é um passo obrigatório.
Neste guia completo, você aprenderá desde os conceitos fundamentais até técnicas avançadas de teste com unittest, incluindo mocks, fixtures e boas práticas que todo desenvolvedor Python precisa conhecer.
O Que São Testes Unitários?
Testes unitários são pequenos programas que verificam o comportamento de unidades individuais do seu código — normalmente funções ou métodos. Cada teste isola uma parte específica do sistema e valida se ela produz o resultado esperado para determinadas entradas.
Diferente dos testes de integração, que verificam como diferentes módulos funcionam juntos, os testes unitários focam em componentes isolados. Essa abordagem permite detectar bugs precocemente, facilita a refatoração e documenta o comportamento esperado do código.
O desenvolvimento orientado a testes (TDD) eleva esse conceito a outro nível: primeiro você escreve o teste que falha, depois implementa o código mínimo para fazê-lo passar e por fim refatora. Esse ciclo conhecido como "Red-Green-Refactor" é amplamente adotado na indústria.
Por Que Usar unittest?
O Python inclui o módulo unittest em sua biblioteca padrão desde a versão 2.1. Inspirado no JUnit (framework de testes do Java), ele oferece uma estrutura robusta e madura para criação de testes. Eis por que você deve considerá-lo:
- Zero dependências externas — já vem instalado com Python, sem necessidade de pip install
- Integração com IDEs — PyCharm, VS Code e outras ferramentas detectam e executam testes unittest nativamente
- Compatibilidade com CI/CD — GitHub Actions, GitLab CI e Jenkins têm suporte direto
- Módulo mock embutido — desde Python 3.3,
unittest.mockfaz parte da biblioteca padrão - Descoberta automática de testes — o descobridor de testes encontra arquivos e métodos seguindo convenções de nomenclatura
- Relatórios detalhados — saída rica com contagem de sucessos, falhas e erros
Embora o pytest seja outra ferramenta popular, o unittest continua sendo a escolha ideal para projetos que priorizam estabilidade e ausência de dependências externas. Grandes projetos como CPython, Django e Plone utilizam unittest extensivamente.
Estrutura Básica de um Teste com unittest
O unittest segue o padrão xUnit, onde os testes são organizados em classes que herdam de unittest.TestCase. Cada método dentro da classe representa um teste individual. A estrutura mínima é:
import unittest
def somar(a, b):
return a + b
class TestSomar(unittest.TestCase):
def test_soma_positivos(self):
self.assertEqual(somar(2, 3), 5)
def test_soma_negativos(self):
self.assertEqual(somar(-1, -1), -2)
if name == 'main':
unittest.main()
O método unittest.main() descobre e executa todos os métodos que começam com test_ dentro das classes que herdam de TestCase. Você pode executar este arquivo diretamente com python meu_teste.py.
Métodos de Asserção (Assert Methods)
Os métodos de asserção são o coração dos testes. O unittest oferece dezenas de métodos para verificar diferentes condições:
self.assertEqual(a, b) # a == b
self.assertNotEqual(a, b) # a != b
self.assertTrue(x) # x é True
self.assertFalse(x) # x é False
self.assertIs(a, b) # a is b
self.assertIsNot(a, b) # a is not b
self.assertIsNone(x) # x is None
self.assertIsNotNone(x) # x is not None
self.assertIn(a, b) # a in b
self.assertNotIn(a, b) # a not in b
self.assertIsInstance(a, b) # isinstance(a, b)
self.assertRaises(Exc, func) # func levanta Exc
self.assertAlmostEqual(a, b) # a ≈ b (para floats)
Para testar exceções, a forma mais elegante é usar o gerenciador de contexto:
with self.assertRaises(ValueError):
int('abc')
Você também pode verificar a mensagem da exceção:
with self.assertRaises(ValueError) as ctx:
int('abc')
self.assertEqual(str(ctx.exception), 'invalid literal for int() with base 10: "abc"')
Fixtures: setUp e tearDown
Fixtures são métodos executados antes e depois de cada teste para preparar e limpar recursos. O unittest oferece quatro níveis de fixtures:
class TestBancoDeDados(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Executado uma vez antes de todos os testes da classe
cls.conexao = conectar_banco()
@classmethod
def tearDownClass(cls):
# Executado uma vez depois de todos os testes
cls.conexao.fechar()
def setUp(self):
# Executado antes de cada teste
self.cursor = self.conexao.cursor()
self.conexao.iniciar_transacao()
def tearDown(self):
# Executado depois de cada teste
self.conexao.rollback()
self.cursor.fechar()
O uso correto de fixtures elimina duplicação e garante que cada teste comece em um estado limpo e previsível. O setUp é ideal para criar objetos, abrir arquivos ou conectar a bancos de dados, enquanto o tearDown deve liberar todos os recursos alocados.
Descoberta e Execução de Testes
O unittest possui um descobridor automático que localiza e executa testes seguindo convenções:
# Executa todos os testes no diretório atual
python -m unittest discover
Executa testes em um diretório específico
python -m unittest discover tests/
Executa testes com padrão de nome específico
python -m unittest discover -p "*_test.py"
Executa um módulo de teste específico
python -m unittest tests/test_calculadora.py
Executa uma classe específica
python -m unittest tests.test_calculadora.TestSoma
Executa um método específico
python -m unittest tests.test_calculadora.TestSoma.test_soma_positivos
A descoberta automática procura por arquivos que correspondam ao padrão test*.py em todo o diretório especificado. Essa funcionalidade é especialmente útil em projetos grandes com centenas de arquivos de teste.
Mocks e Patching com unittest.mock
Em aplicações reais, seu código frequentemente depende de serviços externos — APIs HTTP, bancos de dados, sistemas de arquivos. Testar essas interações diretamente é lento, frágil e impraticável. É aqui que entram os mocks.
O módulo unittest.mock permite substituir partes do seu sistema por objetos simulados durante os testes. O uso mais comum é através do decorador @patch:
from unittest.mock import patch
import requests
def buscar_usuario(api_url, usuario_id):
resposta = requests.get(f'{api_url}/usuarios/{usuario_id}')
return resposta.json()
class TestBuscarUsuario(unittest.TestCase):
@patch('requests.get')
def test_retorna_dados_do_usuario(self, mock_get):
mock_get.return_value.json.return_value = {
'id': 1, 'nome': 'Ana Silva'
}
resultado = buscar_usuario('https://api.exemplo.com', 1)
self.assertEqual(resultado['nome'], 'Ana Silva')
mock_get.assert_called_once_with(
'https://api.exemplo.com/usuarios/1'
)
O patch substitui temporariamente o objeto original pelo mock durante o teste. Após a execução, o objeto original é restaurado automaticamente. Você também pode usar patch como gerenciador de contexto:
def test_com_context_manager(self):
with patch('requests.get') as mock_get:
mock_get.return_value.status_code = 200
resultado = buscar_usuario('https://api.exemplo.com', 1)
self.assertIsNotNone(resultado)
O mock oferece métodos poderosos para verificar interações:
assert_called()— verifica se o mock foi chamadoassert_called_once()— verifica se foi chamado exatamente uma vezassert_called_with(args)— verifica a última chamada com argumentos específicosassert_any_call(args)— verifica se houve alguma chamada com os argumentoscall_count— propriedade que retorna o número total de chamadas
Para classes, use patch.object:
class TestApiClient(unittest.TestCase):
@patch.object(ApiClient, 'enviar_request')
def test_envio_de_dados(self, mock_enviar):
cliente = ApiClient()
cliente.enviar_dados({'chave': 'valor'})
mock_enviar.assert_called_once()
Organizando Testes em Suites
Em projetos grandes, você pode agrupar testes em suites para execução seletiva:
def suite():
suite = unittest.TestSuite()
suite.addTest(TestSoma('test_soma_positivos'))
suite.addTest(TestSubtracao('test_subtrai_negativos'))
return suite
if name == 'main':
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite())
Você também pode criar suites modulares combinando testes de diferentes módulos:
import tests.test_calculadora
import tests.test_usuario
def suite_completa():
loader = unittest.TestLoader()
suite = unittest.TestSuite()
suite.addTests(loader.loadTestsFromModule(tests.test_calculadora))
suite.addTests(loader.loadTestsFromModule(tests.test_usuario))
return suite
Pular Testes e Testes Esperados
O unittest permite pular testes condicionalmente ou marcar falhas esperadas:
class TestAvancado(unittest.TestCase):
@unittest.skip("Funcionalidade ainda não implementada")
def test_novo_recurso(self):
pass
@unittest.skipIf(sys.version_info < (3, 10), "Requer Python 3.10+")
def test_match_case(self):
...
@unittest.skipUnless(os.name == 'posix', "Apenas Unix")
def test_comando_shell(self):
...
@unittest.expectedFailure
def test_bug_conhecido(self):
self.assertEqual(calcular_algo(), valor_esperado)
Esses decoradores são úteis para manter uma suite de testes funcional mesmo quando parte do sistema está em desenvolvimento ou depende de condições específicas de ambiente.
Boas Práticas em Testes Unitários
Escrever bons testes é uma habilidade que vai além da sintaxe do unittest. Aqui estão as práticas mais importantes:
1. Testes Independentes e Isolados
Cada teste deve ser completamente independente dos demais. Um teste nunca deve depender do resultado ou do estado deixado por outro teste. Use mocks para isolar o código de dependências externas.
2. Nomenclatura Clara
O nome do teste deve descrever exatamente o que está sendo verificado e qual o resultado esperado: test_quando_saldo_insuficiente_deve_levantar_excecao é muito mais informativo que test_saque.
3. Um Assert por Conceito
Cada método de teste deve verificar um único comportamento. Se o teste falhar, você saberá exatamente o que está quebrado sem precisar depurar múltiplas asserções.
4. Use Mocks com Moderação
Mocks são ferramentas poderosas, mas o uso excessivo pode tornar os testes frágeis e difíceis de manter. Prefira testar o comportamento real sempre que possível e use mocks apenas para isolar dependências lentas ou imprevisíveis como APIs externas e bancos de dados.
5. Escreva Testes Antes do Código (TDD)
O desenvolvimento orientado a testes força você a pensar no design da API antes da implementação, resultando em código mais limpo e modular. O ciclo "Red-Green-Refactor" é uma prática consagrada.
6. Mantenha os Testes Rápidos
Uma suite de testes que leva horas para executar desencoraja sua execução frequente. Projete os testes para serem rápidos — segundos, não minutos. Testes lentos devem ser movidos para uma categoria separada de testes de integração.
7. Cubra as Bordas
Não teste apenas o "caminho feliz". Teste entradas vazias, valores extremos, tipos inesperados e condições de erro. São nessas bordas que a maioria dos bugs se esconde.
unittest vs pytest: Qual Escolher?
Esta é uma das perguntas mais comuns entre desenvolvedores Python. Ambos os frameworks têm méritos e a escolha depende do contexto do seu projeto.
O unittest é a escolha certa quando você precisa de zero dependências, trabalha em projetos conservadores que priorizam estabilidade ou contribui com projetos da própria Python Software Foundation. Grandes frameworks como Django utilizam unittest como base, e o ecossistema de ferramentas de CI tem suporte nativo.
Já o pytest brilha pela simplicidade e produtividade. Sua sintaxe funcional (sem necessidade de classes) e os fixtures poderosos reduzem drasticamente o boilerplate. O ecossistema de plugins é vasto, e as mensagens de erro são excepcionalmente claras.
Na prática, muitos projetos utilizam ambos: escrevem testes com pytest e utilizam unittest.mock para simular dependências. Você pode ler mais sobre pytest no artigo sobre testes automatizados com pytest.
Medindo Cobertura de Testes
A qualidade dos testes não se mede apenas pela quantidade, mas pela cobertura. A ferramenta coverage.py é a mais utilizada para medir quais linhas do seu código são executadas durante os testes:
# Instalação
pip install coverage
Execução com coverage
coverage run -m unittest discover
Relatório no terminal
coverage report
Relatório HTML detalhado
coverage html
O relatório de cobertura mostra a porcentagem de linhas executadas por arquivo, ajudando a identificar código não testado. No entanto, lembre-se: 100% de cobertura não garante 100% de qualidade. É mais importante ter testes significativos que cubram comportamentos críticos do que simplesmente buscar o número máximo.
Exemplo Prático: Testando uma API REST
Vamos consolidar tudo que aprendemos com um exemplo completo de teste para uma classe que consome uma API REST:
import unittest
from unittest.mock import patch, Mock
import requests
class ClienteAPI:
BASE_URL = 'https://api.exemplo.com/v1'
def __init__(self, token):
self.token = token
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {token}'
})
def buscar_usuario(self, usuario_id):
resposta = self.session.get(
f'{self.BASE_URL}/usuarios/{usuario_id}'
)
resposta.raise_for_status()
return resposta.json()
def criar_usuario(self, dados):
resposta = self.session.post(
f'{self.BASE_URL}/usuarios',
json=dados
)
resposta.raise_for_status()
return resposta.json()
class TestClienteAPI(unittest.TestCase):
def setUp(self):
self.cliente = ClienteAPI('token_valido')
@patch('requests.Session.get')
def test_buscar_usuario_com_sucesso(self, mock_get):
mock_get.return_value.json.return_value = {
'id': 1, 'nome': 'Carlos'
}
mock_get.return_value.raise_for_status = Mock()
resultado = self.cliente.buscar_usuario(1)
self.assertEqual(resultado['nome'], 'Carlos')
mock_get.assert_called_once_with(
'https://api.exemplo.com/v1/usuarios/1'
)
@patch('requests.Session.get')
def test_buscar_usuario_erro_404(self, mock_get):
mock_get.return_value.raise_for_status.side_effect = \
requests.exceptions.HTTPError('404 Not Found')
with self.assertRaises(requests.exceptions.HTTPError):
self.cliente.buscar_usuario(999)
@patch('requests.Session.post')
def test_criar_usuario_com_dados_validos(self, mock_post):
mock_post.return_value.json.return_value = {
'id': 2, 'nome': 'Marina'
}
mock_post.return_value.raise_for_status = Mock()
dados = {'nome': 'Marina', 'email': '[email protected]'}
resultado = self.cliente.criar_usuario(dados)
self.assertEqual(resultado['id'], 2)
mock_post.assert_called_once_with(
'https://api.exemplo.com/v1/usuarios',
json=dados
)
def test_token_eh_atribuido_corretamente(self):
self.assertEqual(
self.cliente.session.headers['Authorization'],
'Bearer token_valido'
)
if name == 'main':
unittest.main()
Este exemplo demonstra na prática os conceitos de setUp, mocks com patch, asserções e teste de diferentes cenários — sucesso, erro e validação de estado.
Integração com CI/CD
Testes unitários são ainda mais poderosos quando executados automaticamente em pipelines de integração contínua. Aqui está um exemplo de configuração para GitHub Actions:
# .github/workflows/testes.yml
name: Testes Python
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- name: Configurar Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Instalar dependências
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Executar testes
run: python -m unittest discover -v
- name: Medir cobertura
run: |
pip install coverage
coverage run -m unittest discover
coverage report
Com essa configuração, seus testes são executados automaticamente em múltiplas versões do Python a cada push ou pull request, garantindo que alterações não quebrem funcionalidades existentes.
Conclusão
O módulo unittest é uma ferramenta madura, confiável e poderosa para testes unitários em Python. Neste guia, você aprendeu desde a estrutura básica de um teste até técnicas avançadas como mocks, fixtures e organização de suites de teste.
Dominar testes unitários é essencial para qualquer desenvolvedor profissional. Eles não apenas previnem regressões, mas também servem como documentação viva do comportamento esperado do sistema. Invista tempo em aprender e aplicar essas técnicas — seu eu do futuro (e sua equipe) agradecerá.
Para continuar seus estudos, confira o guia completo sobre decorators em Python, um recurso fundamental para entender como o @patch e outros decoradores funcionam nos bastidores.
A documentação oficial do módulo unittest é a referência definitiva para consultas detalhadas. O guia oficial de mock também é leitura obrigatória. Para uma abordagem prática complementar, recomendo o tutorial da Real Python sobre testes. A ferramenta coverage.py ajuda a medir a eficácia dos seus testes. O livro Test-Driven Development with Python de Harry Percival é um recurso excelente para aprender TDD na prática. Consulte também o Guia do Mochileiro para Testes em Python para uma visão geral prática. Por fim, veja a documentação do doctest para conhecer outra abordagem de teste embutida no Python.