Mocks
Esta aula aborda o uso de mocks em testes Python, com foco no módulo unittest.mock, na função patch e em como simular dependências para testes isolados e confiáveis. Você aprenderá boas práticas para criar mocks eficazes e evitar armadilhas comuns.
Bem-vindo à aula sobre mocks em Python! Nesta aula, vamos explorar uma das ferramentas mais poderosas para testes: a criação de objetos simulados (mocks) que substituem dependências reais do código sob teste. Isso permite testar unidades de código de forma isolada, sem depender de serviços externos, bancos de dados, APIs ou até mesmo de classes complexas internas.
Mocks são essenciais para escrever testes rápidos, determinísticos e focados. Ao simular o comportamento de dependências, você pode verificar se o código interage corretamente com elas, sem se preocupar com a implementação real. Nesta aula, vamos mergulhar no módulo unittest.mock, na função patch e em boas práticas para usar mocks de forma eficaz.
unittest.mock
O módulo unittest.mock foi introduzido no Python 3.3 e fornece classes e utilitários para criar objetos mock. A classe principal é Mock, que cria objetos que podem ter atributos e métodos definidos dinamicamente. O MagicMock é uma subclasse que suporta métodos mágicos (como __len__, __iter__, etc.), tornando-o mais versátil para simular objetos complexos.
Um objeto mock registra chamadas feitas a ele, permitindo que você faça asserções sobre essas chamadas. Por exemplo, você pode verificar se um método foi chamado com certos argumentos, quantas vezes foi chamado, ou até mesmo definir o valor de retorno. Isso é extremamente útil para testar a interação entre componentes.
Vejamos um exemplo simples de criação e uso de um mock:
from unittest.mock import Mock
# Cria um mock
m = Mock()
# Define um método com retorno específico
m.calcular.return_value = 42
# Chama o método
resultado = m.calcular(10, 20)
# Verifica chamadas
m.calcular.assert_called_once_with(10, 20)
print(resultado) # 42
Nesse exemplo, criamos um mock, definimos o retorno do método calcular e depois verificamos que ele foi chamado com os argumentos corretos. Isso é uma demonstração básica, mas mostra o poder dos mocks.
patch
A função patch é usada para substituir temporariamente um objeto em um módulo ou classe por um mock. Ela pode ser usada como decorador ou como context manager. O alvo do patch pode ser uma string no formato 'pacote.modulo.objeto' ou um objeto. O patch substitui o objeto durante a execução do bloco ou da função decorada e o restaura após o término.
Existem três formas comuns de usar o patch:
- Como decorador de função:
@patch('modulo.funcao') - Como context manager:
with patch('modulo.funcao') as mock_funcao: - Com
patch.objectpara métodos de uma classe.
Vamos ver um exemplo de uso de patch para substituir uma função externa:
# arquivo: meu_modulo.py
def buscar_dados():
# Simula uma chamada a uma API
return {"dados": [1, 2, 3]}
def processar():
dados = buscar_dados()
return sum(dados["dados"])
No teste, podemos usar patch para substituir buscar_dados por um mock que retorna dados controlados:
from unittest.mock import patch
from meu_modulo import processar
with patch('meu_modulo.buscar_dados') as mock_buscar:
mock_buscar.return_value = {"dados": [10, 20, 30]}
resultado = processar()
assert resultado == 60
O patch substitui a função no módulo meu_modulo pelo mock, e o mock retorna o valor definido. Assim, podemos testar processar sem depender da implementação real de buscar_dados.
Mockando dependências
Muitas vezes, o código depende de serviços externos, como APIs, bancos de dados ou classes de infraestrutura. Mockar essas dependências permite testar a lógica de negócio isoladamente, sem precisar de acesso a esses serviços. Isso torna os testes mais rápidos e confiáveis, pois não dependem de rede ou de estado externo.
Por exemplo, considere uma classe que envia emails:
class EmailService:
def enviar(self, destinatario, mensagem):
# Simula envio real
return True
class Notificador:
def __init__(self, email_service):
self.email_service = email_service
def notificar(self, usuario, mensagem):
self.email_service.enviar(usuario.email, mensagem)
Para testar Notificador, podemos criar um mock de EmailService e verificar se o método enviar é chamado corretamente:
from unittest.mock import MagicMock
class Usuario:
def __init__(self, email):
self.email = email
email_service = MagicMock()
notificador = Notificador(email_service)
usuario = Usuario("user@example.com")
notificador.notificar(usuario, "Olá!")
email_service.enviar.assert_called_once_with("user@example.com", "Olá!")
Nesse exemplo, usamos MagicMock para criar um mock de EmailService e verificamos que o método enviar foi chamado com os argumentos esperados. Isso nos permite testar a lógica de Notificador sem enviar emails reais.
Outra técnica comum é usar patch para substituir uma classe inteira ou um objeto importado. Por exemplo, se um módulo importa uma classe de outro módulo, podemos usar patch para substituir essa classe por um mock que simula seu comportamento.
# arquivo: servico.py
from conexao import Conexao
class Servico:
def __init__(self):
self.conexao = Conexao()
def realizar_operacao(self):
return self.conexao.executar("SELECT * FROM tabela")
No teste, podemos patch a classe Conexao para retornar dados falsos:
from unittest.mock import patch
with patch('servico.Conexao') as MockConexao:
instancia = MockConexao.return_value
instancia.executar.return_value = [{"id": 1}]
servico = Servico()
resultado = servico.realizar_operacao()
assert resultado == [{"id": 1}]
Nesse exemplo, patch substitui a classe Conexao dentro do módulo servico por um mock. Quando Servico é instanciado, ele usa o mock da classe Conexao, e o método executar retorna o valor definido.
Boas práticas
Usar mocks de forma eficaz requer algumas boas práticas para evitar testes frágeis e que não reflitam a realidade. Aqui estão algumas recomendações:
- Mocke apenas o que é externo: Não crie mocks para código interno que você pode testar diretamente. Mocks são para dependências que você não controla, como APIs, bancos de dados, ou classes de terceiros.
- Prefira
patcha criar mocks manualmente: A funçãopatchsubstitui objetos no local onde são usados, o que é mais realista do que injetar mocks manualmente. - Use
MagicMockquando precisar de métodos mágicos:MagicMocksuporta operações como__len__,__iter__, etc., úteis para simular objetos que serão usados em contextos específicos. - Verifique as chamadas com precisão: Use
assert_called_once_withouassert_any_callpara garantir que as interações estão corretas. - Evite mocks excessivos: Se você está mockando muitas coisas, pode estar testando apenas a interação entre mocks, não a lógica real. Tente manter o teste focado.
- Use
specpara limitar atributos: Ao criar um mock, você pode especificarspec=ClasseRealpara que o mock apenas aceite atributos que existem na classe real, evitando erros de digitação.
from unittest.mock import MagicMock
class MinhaClasse:
def metodo_real(self):
pass
mock = MagicMock(spec=MinhaClasse)
mock.metodo_real() # OK
mock.metodo_inexistente() # AttributeError
Além disso, é importante lembrar que mocks são ferramentas para testes, não para produção. Não use mocks no código de produção. Eles devem ser usados apenas nos testes para simular dependências.
Boas práticas adicionais
Uma boa prática é usar mocks para testar o comportamento de funções que dependem de tempo, como time.sleep ou datetime.now. Isso permite que você controle o tempo no teste, tornando-o determinístico.
from unittest.mock import patch
from datetime import datetime
def obter_ano_atual():
return datetime.now().year
with patch('datetime.datetime') as mock_datetime:
mock_datetime.now.return_value = datetime(2020, 1, 1)
assert obter_ano_atual() == 2020
Outra prática é usar mocks para simular erros e exceções, garantindo que o código lida com falhas corretamente.
from unittest.mock import patch
import requests
def buscar_usuario(user_id):
response = requests.get(f"https://api.exemplo.com/users/{user_id}")
response.raise_for_status()
return response.json()
with patch('requests.get') as mock_get:
mock_get.side_effect = requests.exceptions.RequestException("Erro de rede")
try:
buscar_usuario(1)
except requests.exceptions.RequestException:
print("Erro capturado")
Isso mostra como testar a robustez do código diante de falhas externas.
Referências
- Documentação oficial do unittest.mock
- Exemplos de unittest.mock
- Python Mock Library: Real Python
- Documentação do unittest
- Python Testing with pytest (O'Reilly)
- pytest-mock documentation
Exercícios
- Exercício 1: Crie um mock simples de uma função que retorna a soma de dois números. Verifique se a função foi chamada com os argumentos corretos e se o retorno é o esperado.✓ Resposta:
from unittest.mock import Mock # Cria mock mock_soma = Mock() mock_soma.return_value = 5 # Chama resultado = mock_soma(2, 3) # Verifica mock_soma.assert_called_once_with(2, 3) assert resultado == 5 print(resultado) # 5 - Exercício 2: Usando
patch, substitua a funçãotime.timepara retornar um valor fixo e teste uma função que usa o tempo atual.✓ Resposta:import time from unittest.mock import patch def obter_timestamp(): return int(time.time()) with patch('time.time', return_value=1600000000): assert obter_timestamp() == 1600000000 print("Teste passou!") - Exercício 3: Crie uma classe que dependa de um serviço de banco de dados. Use
MagicMockpara simular o serviço e verifique se o método de salvar é chamado corretamente.✓ Resposta:from unittest.mock import MagicMock class BancoDeDados: def salvar(self, dados): pass class Repositorio: def __init__(self, db): self.db = db def salvar_usuario(self, usuario): self.db.salvar(usuario) db_mock = MagicMock() repo = Repositorio(db_mock) usuario = {"nome": "João"} repo.salvar_usuario(usuario) db_mock.salvar.assert_called_once_with(usuario) print("Teste passou!") - Exercício 4: Use
patchpara substituir uma classe externa em um módulo e teste uma função que a utiliza. Verifique se o método da classe mockada foi chamado.✓ Resposta:# mod.py class ClienteExterno: def obter_dados(self): return "dados reais" def processar(): cliente = ClienteExterno() return cliente.obter_dados() # teste from unittest.mock import patch with patch('mod.ClienteExterno') as MockCliente: instancia = MockCliente.return_value instancia.obter_dados.return_value = "dados mockados" resultado = processar() assert resultado == "dados mockados" instancia.obter_dados.assert_called_once_with() print("Teste passou!") - Exercício 5: Escreva um teste que use
side_effectpara simular uma exceção em uma chamada de API e verifique se a função lida com o erro adequadamente.✓ Resposta:import requests from unittest.mock import patch def buscar_dados(url): try: response = requests.get(url) response.raise_for_status() return response.json() except requests.exceptions.RequestException: return None with patch('requests.get') as mock_get: mock_get.side_effect = requests.exceptions.RequestException("Erro") resultado = buscar_dados("https://api.exemplo.com") assert resultado is None mock_get.assert_called_once_with("https://api.exemplo.com") print("Teste passou!")