Testes com unittest: fundamentos
1. Introdução ao módulo unittest
O módulo unittest é o framework de testes unitários nativo do Python, presente na biblioteca padrão desde a versão 2.1. Inspirado no JUnit (Java) e na filosofia xUnit, ele fornece uma estrutura robusta para criar, organizar e executar testes automatizados. Diferentemente do pytest, que adota uma abordagem mais concisa com funções simples, o unittest segue um modelo orientado a objetos, exigindo que os testes sejam escritos como métodos dentro de classes que herdam de TestCase.
A estrutura básica de um arquivo de teste consiste em:
- Importação do módulo unittest
- Criação de uma classe que herda de unittest.TestCase
- Definição de métodos que começam com test_
import unittest
class TestExemplo(unittest.TestCase):
def test_soma(self):
self.assertEqual(1 + 1, 2)
if __name__ == '__main__':
unittest.main()
2. Escrevendo o primeiro caso de teste
Para criar um caso de teste, basta definir métodos dentro da classe de teste com o prefixo test_. O unittest identifica automaticamente esses métodos e os executa. Os métodos de asserção mais comuns incluem:
import unittest
def calcular_area(base, altura):
return base * altura
def validar_email(email):
return '@' in email and '.' in email.split('@')[-1]
class TestCalculadora(unittest.TestCase):
def test_area_retangulo(self):
self.assertEqual(calcular_area(5, 3), 15)
def test_area_zero(self):
self.assertEqual(calcular_area(0, 10), 0)
def test_email_valido(self):
self.assertTrue(validar_email('usuario@exemplo.com'))
def test_email_invalido(self):
self.assertFalse(validar_email('usuarioexemplo.com'))
def test_divisao_por_zero(self):
with self.assertRaises(ZeroDivisionError):
10 / 0
if __name__ == '__main__':
unittest.main()
Para executar os testes, utilize o terminal:
python -m unittest test_calculadora.py
Ou, para descoberta automática em um diretório:
python -m unittest discover
3. Organização e ciclo de vida dos testes
O unittest oferece métodos especiais para gerenciar o ciclo de vida dos testes:
import unittest
import os
import tempfile
class TestArquivo(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Executado uma vez antes de todos os testes da classe"""
cls.diretorio_temp = tempfile.mkdtemp()
@classmethod
def tearDownClass(cls):
"""Executado uma vez após todos os testes da classe"""
os.rmdir(cls.diretorio_temp)
def setUp(self):
"""Executado antes de cada teste"""
self.arquivo = os.path.join(self.diretorio_temp, 'teste.txt')
with open(self.arquivo, 'w') as f:
f.write('dados iniciais')
def tearDown(self):
"""Executado após cada teste"""
if os.path.exists(self.arquivo):
os.remove(self.arquivo)
def test_leitura_arquivo(self):
with open(self.arquivo, 'r') as f:
conteudo = f.read()
self.assertEqual(conteudo, 'dados iniciais')
def test_escrita_arquivo(self):
with open(self.arquivo, 'a') as f:
f.write('\nnovos dados')
with open(self.arquivo, 'r') as f:
linhas = f.readlines()
self.assertEqual(len(linhas), 2)
if __name__ == '__main__':
unittest.main()
Boas práticas importantes:
- Cada teste deve ser independente e não depender do resultado de outro teste
- Use setUp() para criar objetos reutilizáveis
- Use tearDown() para limpar recursos (arquivos, conexões)
- A ordem de execução dos testes não é garantida
4. Asserções avançadas e mensagens personalizadas
O unittest oferece asserções especializadas para diferentes tipos de dados:
import unittest
class TestAssercoesAvancadas(unittest.TestCase):
def test_listas(self):
self.assertListEqual([1, 2, 3], [1, 2, 3])
self.assertIn(2, [1, 2, 3])
self.assertNotIn(4, [1, 2, 3])
def test_dicionarios(self):
self.assertDictEqual({'a': 1, 'b': 2}, {'b': 2, 'a': 1})
self.assertIn('a', {'a': 1, 'b': 2})
def test_excecoes_com_contexto(self):
with self.assertRaises(ValueError) as contexto:
int('abc')
self.assertEqual(str(contexto.exception), "invalid literal for int() with base 10: 'abc'")
def test_mensagem_personalizada(self):
resultado = 2 + 2
self.assertEqual(resultado, 5, f"Esperado 5, mas obtive {resultado}")
def test_aproximacao_numerica(self):
self.assertAlmostEqual(3.14159, 3.1416, places=3)
def test_identidade(self):
lista = [1, 2, 3]
self.assertIs(lista, lista) # mesmo objeto
self.assertIsNot(lista, [1, 2, 3]) # objetos diferentes
if __name__ == '__main__':
unittest.main()
5. Testes com dependências externas (mocking básico)
O módulo unittest.mock permite simular dependências externas, como APIs, bancos de dados ou sistemas de arquivos:
import unittest
from unittest.mock import patch, MagicMock
import requests
def buscar_dados_api(url):
resposta = requests.get(url)
if resposta.status_code == 200:
return resposta.json()
return None
def processar_usuario(usuario_id):
dados = buscar_dados_api(f'https://api.exemplo.com/usuarios/{usuario_id}')
if dados:
return f"Usuário: {dados['nome']}, Email: {dados['email']}"
return "Usuário não encontrado"
class TestMocking(unittest.TestCase):
@patch('requests.get')
def test_buscar_dados_sucesso(self, mock_get):
mock_resposta = MagicMock()
mock_resposta.status_code = 200
mock_resposta.json.return_value = {'nome': 'João', 'email': 'joao@exemplo.com'}
mock_get.return_value = mock_resposta
resultado = buscar_dados_api('https://api.exemplo.com/usuarios/1')
self.assertEqual(resultado, {'nome': 'João', 'email': 'joao@exemplo.com'})
mock_get.assert_called_once_with('https://api.exemplo.com/usuarios/1')
@patch('test_mocking.buscar_dados_api')
def test_processar_usuario(self, mock_buscar):
mock_buscar.return_value = {'nome': 'Maria', 'email': 'maria@exemplo.com'}
resultado = processar_usuario(2)
self.assertEqual(resultado, "Usuário: Maria, Email: maria@exemplo.com")
self.assertEqual(mock_buscar.call_count, 1)
@patch('test_mocking.buscar_dados_api')
def test_usuario_nao_encontrado(self, mock_buscar):
mock_buscar.return_value = None
resultado = processar_usuario(999)
self.assertEqual(resultado, "Usuário não encontrado")
if __name__ == '__main__':
unittest.main()
6. Organizando suites de teste e descoberta automática
Para projetos maiores, é essencial organizar os testes em suites e utilizar a descoberta automática:
# test_suites.py
import unittest
from test_calculadora import TestCalculadora
from test_arquivo import TestArquivo
from test_mocking import TestMocking
def criar_suite():
suite = unittest.TestSuite()
# Adicionando testes individuais
suite.addTest(TestCalculadora('test_area_retangulo'))
suite.addTest(TestArquivo('test_leitura_arquivo'))
# Adicionando todos os testes de uma classe
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMocking))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner()
runner.run(criar_suite())
Estrutura de diretórios recomendada para projetos reais:
projeto/
├── src/
│ ├── __init__.py
│ ├── calculadora.py
│ └── usuario.py
├── tests/
│ ├── __init__.py
│ ├── test_calculadora.py
│ ├── test_usuario.py
│ └── test_suites.py
└── requirements.txt
Para descoberta automática:
python -m unittest discover -s tests -p "test_*.py" -v
7. Dicas de boas práticas e armadilhas comuns
import unittest
# BOA PRÁTICA: Nomeação clara
class TestSistemaAutenticacao(unittest.TestCase):
def test_usuario_valido_autentica_com_sucesso(self):
pass
def test_senha_invalida_retorna_erro(self):
pass
# ARMADILHA: Dependência entre testes
contador_global = 0
class TestComEstadoGlobal(unittest.TestCase):
def test_incremento(self):
global contador_global
contador_global += 1
self.assertEqual(contador_global, 1) # Funciona isoladamente
def test_outro_incremento(self):
global contador_global
self.assertEqual(contador_global, 0) # FALHA! Depende da ordem
# CORREÇÃO: Isolamento
class TestCorrigido(unittest.TestCase):
def setUp(self):
self.contador = 0
def test_incremento(self):
self.contador += 1
self.assertEqual(self.contador, 1)
def test_outro_incremento(self):
self.assertEqual(self.contador, 0) # Sempre 0, independente
# ARMADILHA: assertEqual vs assertIs
class TestIdentidade(unittest.TestCase):
def test_igualdade(self):
self.assertEqual([1, 2, 3], [1, 2, 3]) # OK: mesmo conteúdo
def test_identidade(self):
self.assertIs([1, 2, 3], [1, 2, 3]) # FALHA: objetos diferentes
def test_identidade_correta(self):
lista = [1, 2, 3]
self.assertIs(lista, lista) # OK: mesmo objeto
if __name__ == '__main__':
unittest.main()
Referências
- Documentação oficial do unittest — Guia completo e referência da API do módulo unittest na biblioteca padrão do Python.
- Documentação do unittest.mock — Referência oficial para criação de mocks, stubs e spies em testes unitários.
- Real Python: Getting Started With Testing in Python — Tutorial prático sobre testes em Python, incluindo unittest e boas práticas.
- Python Testing with unittest: A Practical Guide — Guia passo a passo da DataCamp com exemplos práticos de testes unitários.
- Testing Python Applications with pytest (comparação conceitual) — Documentação do pytest, framework concorrente, útil para entender as diferenças com unittest.
- Effective Python Testing with unittest — Artigo técnico sobre estratégias avançadas de teste com unittest e padrões de projeto.