Mocks
Nesta aula, você aprenderá a usar mocks em Python para isolar testes de unidades, controlando dependências externas. Exploraremos o módulo unittest.mock, a função patch, e como aplicar mocks em cenários reais, além de boas práticas para escrever testes eficazes.
Mocks são objetos simulados que imitam o comportamento de objetos reais de forma controlada. Em testes de unidade, eles são essenciais para isolar o código sob teste, substituindo dependências como chamadas de API, acesso a banco de dados ou operações de I/O. Isso permite verificar se o código interage corretamente com essas dependências, sem depender de sua implementação real.
Nesta aula, vamos explorar o módulo unittest.mock da biblioteca padrão do Python, que fornece ferramentas poderosas para criar e gerenciar mocks. Você aprenderá a usar a função patch para substituir objetos em módulos, a configurar retornos e efeitos colaterais, e a verificar chamadas. Também discutiremos boas práticas para evitar armadilhas comuns.
unittest.mock
O módulo unittest.mock é uma biblioteca de mocking integrada ao Python desde a versão 3.3. Ele fornece classes como Mock, MagicMock, patch, entre outras, para criar objetos falsos que podem ser usados em testes. A classe Mock é a base: ela aceita qualquer chamada de atributo ou método e retorna outro Mock por padrão, permitindo que você configure comportamentos específicos.
Um dos recursos mais úteis é a capacidade de configurar atributos e métodos de retorno. Por exemplo, você pode definir que um método de um mock retorne um valor específico quando chamado. Além disso, mocks registram todas as chamadas feitas a eles, permitindo que você faça asserções sobre como o código interage com a dependência.
Vamos ver um exemplo básico:
from unittest.mock import Mock
# Cria um mock
m = Mock()
# Configura o retorno de um método
m.some_method.return_value = 42
# Chama o método
result = m.some_method()
# Verifica o resultado
print(result) # Saída: 42
# Verifica se o método foi chamado
m.some_method.assert_called_once()
Além de Mock, existe MagicMock, que fornece implementações padrão de métodos mágicos (como __len__, __iter__, etc.). Isso é útil quando o objeto mockado precisa suportar protocolos especiais, como iteração ou acesso a itens.
Outra funcionalidade importante é a capacidade de configurar efeitos colaterais com side_effect. Você pode passar uma função que será chamada em vez do retorno, permitindo simular exceções ou comportamentos dinâmicos.
from unittest.mock import Mock
m = Mock()
m.method.side_effect = ValueError("Erro simulado")
try:
m.method()
except ValueError as e:
print(e) # Saída: Erro simulado
patch
A função patch é usada para substituir temporariamente objetos em módulos ou classes durante um teste. Ela pode ser usada como decorador ou como gerenciador de contexto. O alvo do patch é uma string que identifica o objeto a ser substituído, no formato 'pacote.modulo.objeto'.
Quando o teste termina, o patch é revertido automaticamente, restaurando o objeto original. Isso garante que os testes não interfiram uns nos outros.
Exemplo com decorador:
from unittest.mock import patch
@patch('meu_modulo.minha_funcao')
def test_com_patch(mock_funcao):
mock_funcao.return_value = 10
# Código que usa minha_funcao
from meu_modulo import usa_funcao
resultado = usa_funcao()
assert resultado == 10
Exemplo com gerenciador de contexto:
from unittest.mock import patch
with patch('meu_modulo.minha_funcao') as mock_funcao:
mock_funcao.return_value = 10
# Código que usa minha_funcao
from meu_modulo import usa_funcao
resultado = usa_funcao()
assert resultado == 10
O patch também pode ser usado para substituir classes inteiras, ou para criar mocks automáticos de objetos. Por exemplo, patch('requests.get') substitui a função get do módulo requests, permitindo simular respostas HTTP sem fazer chamadas reais.
Mockando dependências
Em muitos casos, o código que você testa depende de serviços externos, como APIs, bancos de dados ou arquivos. Mockar essas dependências permite que você teste a lógica do seu código de forma isolada, garantindo que os testes sejam rápidos e confiáveis.
Vamos considerar um exemplo prático: uma função que faz uma requisição HTTP e processa a resposta. Em vez de fazer uma chamada real, usamos patch para simular a resposta.
import requests
def obter_dados(api_url):
resposta = requests.get(api_url)
if resposta.status_code == 200:
return resposta.json()
else:
raise Exception("Erro na requisição")
Para testar essa função sem acessar a internet, patcheamos requests.get:
from unittest.mock import patch, Mock
import minha_api
def test_obter_dados_sucesso():
mock_resposta = Mock()
mock_resposta.status_code = 200
mock_resposta.json.return_value = {"chave": "valor"}
with patch('requests.get', return_value=mock_resposta) as mock_get:
resultado = minha_api.obter_dados('http://exemplo.com/api')
assert resultado == {"chave": "valor"}
mock_get.assert_called_once_with('http://exemplo.com/api')
Outro cenário comum é mockar uma classe de banco de dados. Suponha que você tenha uma classe Repositorio com métodos para salvar e recuperar dados. Em testes, você pode substituir a classe por um mock e verificar se os métodos são chamados corretamente.
from unittest.mock import patch
def salvar_usuario(repositorio, usuario):
repositorio.salvar(usuario)
# No teste
with patch('meu_modulo.Repositorio') as MockRepositorio:
repositorio = MockRepositorio()
salvar_usuario(repositorio, {"nome": "Ana"})
repositorio.salvar.assert_called_once_with({"nome": "Ana"})
É importante destacar que, ao mockar dependências, você deve garantir que o comportamento simulado seja realista o suficiente para que o teste seja significativo. Por exemplo, se você está testando um tratamento de erro, deve configurar o mock para lançar uma exceção.
Boas práticas
O uso de mocks deve ser feito com moderação e critério. Aqui estão algumas boas práticas para escrever testes eficazes:
- Mocke apenas o que é necessário: Não mocke tudo indiscriminadamente. Foque nas dependências externas que tornam o teste lento, não determinístico ou difícil de configurar.
- Prefira mocks a monkey patching manual: O módulo
unittest.mockfornece ferramentas seguras e gerenciadas para substituir objetos, evitando problemas de escopo e restauração. - Use
MagicMockquando precisar de métodos mágicos: Se o objeto mockado for usado em operações comolen()ou iteração,MagicMockfornece implementações padrão. - Verifique chamadas com asserções: Use
assert_called_once_with,assert_any_call, etc., para garantir que o código interage corretamente com a dependência. - Evite mockar detalhes internos: Mocke a interface pública, não implementações internas. Isso torna os testes mais robustos a mudanças.
- Mantenha os testes legíveis: Configure mocks de forma clara e organizada, usando helpers se necessário.
- Considere usar
patchcomo decorador para simplificar: Reduz a indentação e torna o teste mais direto. - Não teste a implementação do mock em si: Lembre-se de que você está testando o seu código, não o mock.
Além disso, é importante entender a diferença entre mocks, stubs e fakes. Mocks são usados para verificar interações, stubs para fornecer respostas prontas e fakes são implementações simplificadas que funcionam. Em testes de unidade, muitas vezes usamos stubs (configurando retornos) e mocks (verificando chamadas) juntos.
Referências
- Documentação oficial do unittest.mock
- Documentação do unittest
- Real Python: Python Mock Library
- Exemplos de uso do unittest.mock
- Mocks Aren't Stubs - Martin Fowler
- Python Testing Guide
Exercícios
-
Crie um mock de uma função chamada
get_dataque retorne um dicionário qualquer. Verifique se a função foi chamada exatamente uma vez e com o argumento"chave".✓ Resposta:from unittest.mock import Mock mock = Mock(return_value={"dado": 1}) resultado = mock("chave") assert resultado == {"dado": 1} mock.assert_called_once_with("chave") -
Utilize
patchpara substituir a funçãoos.getcwdem um teste e faça com que retorne"/tmp". Verifique se a função foi chamada.✓ Resposta:from unittest.mock import patch with patch('os.getcwd', return_value='/tmp') as mock_getcwd: import os caminho = os.getcwd() assert caminho == '/tmp' mock_getcwd.assert_called_once() -
Escreva um teste para a função
somaque recebe dois números e os soma. Use um mock para simular uma dependência externa que não existe (por exemplo, um módulo chamadocalculadora).✓ Resposta:from unittest.mock import patch def soma(a, b): from calculadora import somar return somar(a, b) with patch('calculadora.somar', return_value=5) as mock_somar: resultado = soma(2, 3) assert resultado == 5 mock_somar.assert_called_once_with(2, 3) -
Crie um mock de uma classe
BancoDeDadoscom um métodoconectarque levanta uma exceção. Teste que o código que usa essa classe trata a exceção corretamente.✓ Resposta:from unittest.mock import Mock class BancoDeDados: def conectar(self): pass def conectar_com_tratamento(): try: db = BancoDeDados() db.conectar() return "conectado" except Exception: return "erro" mock_db = Mock() mock_db.conectar.side_effect = Exception("Falha") with patch('__main__.BancoDeDados', return_value=mock_db): resultado = conectar_com_tratamento() assert resultado == "erro" -
Usando
patchcomo decorador, teste uma função que chamarequests.gete retorna o conteúdo JSON. Simule uma resposta com status 200 e dados específicos.✓ Resposta:import requests from unittest.mock import patch, Mock def obter_json(url): resposta = requests.get(url) return resposta.json() @patch('requests.get') def test_obter_json(mock_get): mock_resposta = Mock() mock_resposta.status_code = 200 mock_resposta.json.return_value = {"mensagem": "ok"} mock_get.return_value = mock_resposta resultado = obter_json('http://exemplo.com') assert resultado == {"mensagem": "ok"} mock_get.assert_called_once_with('http://exemplo.com')
Observações finais
Mocks são ferramentas poderosas para testes, mas devem ser usados com discernimento. Lembre-se de que o objetivo é testar o comportamento do seu código, não o mock em si. Sempre que possível, prefira testes de integração para validar a interação real com dependências, reservando mocks para testes de unidade que exigem isolamento.
Além disso, a partir do Python 3.3, unittest.mock é a biblioteca padrão, mas existem alternativas como pytest-mock que integram mocks de forma mais elegante com o framework pytest. Vale a pena explorar essas opções em projetos reais.