Mocking e patching de dependências externas
1. Introdução ao Mocking em Python
Mocking é uma técnica fundamental em testes de software que permite substituir componentes reais por objetos simulados. Em Python, mocks são objetos que imitam o comportamento de dependências externas, permitindo testar unidades de código isoladamente.
O que são mocks, stubs e fakes?
- Mock: objeto que simula comportamento e registra interações
- Stub: objeto que fornece respostas pré-definidas sem registrar chamadas
- Fake: implementação simplificada que funciona, mas não é adequada para produção
Quando mockar dependências externas:
- Chamadas a APIs REST
- Operações em banco de dados
- Acesso a sistemas de arquivos
- Serviços de terceiros
- Operações de rede
O objetivo principal é eliminar dependências lentas, instáveis ou indisponíveis durante os testes.
2. A Biblioteca unittest.mock
A biblioteca unittest.mock (disponível desde Python 3.3) fornece as classes Mock e MagicMock.
A classe Mock
from unittest.mock import Mock
# Criando um mock básico
mock_obj = Mock()
mock_obj.qualquer_metodo.return_value = 42
resultado = mock_obj.qualquer_metodo("argumento")
print(resultado) # 42
# Atributos dinâmicos
mock_obj.atributo_qualquer = "valor"
print(mock_obj.atributo_qualquer) # "valor"
A classe MagicMock
MagicMock herda de Mock e implementa métodos mágicos do Python:
from unittest.mock import MagicMock
mock_magico = MagicMock()
mock_magico.__len__.return_value = 5
mock_magico.__iter__.return_value = iter([1, 2, 3])
print(len(mock_magico)) # 5
print(list(mock_magico)) # [1, 2, 3]
Configuração de retornos e efeitos colaterais
from unittest.mock import Mock
# return_value: valor fixo de retorno
mock = Mock()
mock.metodo.return_value = "sucesso"
# side_effect: função, exceção ou iterável
def processar(arg):
return f"processado: {arg}"
mock.outro_metodo.side_effect = processar
print(mock.outro_metodo("teste")) # "processado: teste"
# side_effect pode levantar exceções
mock.erro.side_effect = ValueError("erro simulado")
3. Patching com patch
unittest.mock.patch permite substituir objetos em escopos específicos.
Como decorador e gerenciador de contexto
from unittest.mock import patch
import requests
# Como decorador
@patch('requests.get')
def test_com_decorador(mock_get):
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"dados": "teste"}
resposta = requests.get('https://api.exemplo.com')
assert resposta.status_code == 200
# Como gerenciador de contexto
def test_com_contexto():
with patch('requests.get') as mock_get:
mock_get.return_value.status_code = 404
resposta = requests.get('https://api.exemplo.com')
assert resposta.status_code == 404
Escopo do patch
# Patch em nível de módulo
@patch('modulo_externo.funcao')
def test_funcao_patchada(mock_funcao):
pass
# Patch em classe inteira
@patch('modulo_externo.ClasseExterna')
def test_classe_patchada(MockClasse):
instancia = MockClasse.return_value
instancia.metodo.return_value = "mockado"
Exemplo prático: mockando uma API REST
import requests
from unittest.mock import patch
def buscar_usuario(usuario_id):
resposta = requests.get(f'https://api.exemplo.com/usuarios/{usuario_id}')
if resposta.status_code == 200:
return resposta.json()
return None
@patch('requests.get')
def test_buscar_usuario_sucesso(mock_get):
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"id": 1, "nome": "João", "email": "joao@exemplo.com"
}
resultado = buscar_usuario(1)
assert resultado["nome"] == "João"
mock_get.assert_called_once_with('https://api.exemplo.com/usuarios/1')
@patch('requests.get')
def test_buscar_usuario_erro(mock_get):
mock_get.return_value.status_code = 404
resultado = buscar_usuario(999)
assert resultado is None
4. Verificando Interações com Mocks
from unittest.mock import Mock
mock = Mock()
# Chamando métodos para testar
mock.metodo("arg1")
mock.metodo("arg2")
mock.outro_metodo()
# Asserts de chamada
mock.metodo.assert_called()
mock.metodo.assert_called_once()
mock.metodo.assert_called_with("arg1")
mock.metodo.assert_any_call("arg2")
mock.assert_has_calls([
mock.metodo("arg1"),
mock.metodo("arg2"),
mock.outro_metodo()
])
# Atributos de inspeção
print(f"Total de chamadas: {mock.metodo.call_count}") # 2
print(f"Últimos argumentos: {mock.metodo.call_args}") # call('arg2')
print(f"Todas as chamadas: {mock.metodo.call_args_list}")
5. Mockando Dependências Externas Específicas
Mockando requisições HTTP
from unittest.mock import patch, Mock
import requests
def processar_api():
response = requests.post('https://api.exemplo.com/dados',
json={"chave": "valor"},
headers={"Authorization": "Bearer token"})
return response.json()
@patch('requests.post')
def test_processar_api(mock_post):
mock_response = Mock()
mock_response.status_code = 201
mock_response.json.return_value = {"id": 123}
mock_post.return_value = mock_response
resultado = processar_api()
assert resultado["id"] == 123
mock_post.assert_called_once_with(
'https://api.exemplo.com/dados',
json={"chave": "valor"},
headers={"Authorization": "Bearer token"}
)
Mockando acesso a banco de dados
from unittest.mock import patch, MagicMock
def buscar_produtos(conexao):
cursor = conexao.cursor()
cursor.execute("SELECT * FROM produtos")
return cursor.fetchall()
@patch('database.Connection')
def test_buscar_produtos(MockConnection):
mock_conexao = MockConnection.return_value
mock_cursor = MagicMock()
mock_conexao.cursor.return_value = mock_cursor
mock_cursor.fetchall.return_value = [
(1, "Produto A", 50.0),
(2, "Produto B", 30.0)
]
produtos = buscar_produtos(mock_conexao)
assert len(produtos) == 2
mock_cursor.execute.assert_called_once_with("SELECT * FROM produtos")
Mockando operações de sistema de arquivos
from unittest.mock import patch, mock_open
def ler_config():
with open('config.txt', 'r') as f:
return f.read()
@patch('builtins.open', new_callable=mock_open, read_data="config=produção")
def test_ler_config(mock_file):
resultado = ler_config()
assert resultado == "config=produção"
mock_file.assert_called_once_with('config.txt', 'r')
6. Técnicas Avançadas de Mocking
spec e autospec
from unittest.mock import Mock, patch
class ServicoReal:
def processar(self, dados):
return f"processando {dados}"
# spec garante que apenas métodos existentes sejam chamados
mock_com_spec = Mock(spec=ServicoReal)
mock_com_spec.processar("teste") # OK
# mock_com_spec.metodo_inexistente() # AttributeError
# autospec verifica assinatura de métodos
@patch('modulo.ServicoReal', autospec=True)
def test_com_autospec(MockServico):
instancia = MockServico.return_value
instancia.processar("dados") # Assinatura correta
# instancia.processar() # TypeError: argumentos incorretos
PropertyMock para propriedades
from unittest.mock import PropertyMock, patch
class Configuracao:
@property
def ambiente(self):
return "produção"
@patch('modulo.Configuracao.ambiente', new_callable=PropertyMock)
def test_ambiente(mock_ambiente):
mock_ambiente.return_value = "teste"
config = Configuracao()
assert config.ambiente == "teste"
patch.object e patch.multiple
from unittest.mock import patch
class Servico:
def metodo1(self):
return "original1"
def metodo2(self):
return "original2"
# patch.object para método específico
with patch.object(Servico, 'metodo1', return_value="mockado"):
s = Servico()
print(s.metodo1()) # "mockado"
print(s.metodo2()) # "original2"
# patch.multiple para múltiplos métodos
with patch.multiple(Servico, metodo1=Mock(), metodo2=Mock()):
s = Servico()
s.metodo1()
s.metodo2()
7. Boas Práticas e Armadilhas Comuns
Evitar over-mocking:
- Mock apenas dependências externas, não lógica interna
- Prefira testar com dados reais quando possível
- Use fixtures para simplificar a configuração
Limpeza de patches:
import unittest
from unittest.mock import patch
class TesteExemplo(unittest.TestCase):
def setUp(self):
self.patcher = patch('requests.get')
self.mock_get = self.patcher.start()
def tearDown(self):
self.patcher.stop()
def test_algo(self):
self.mock_get.return_value.status_code = 200
# teste aqui
Problemas com importação:
# ERRADO: patch no módulo importado
from modulo_externo import funcao
@patch('modulo_externo.funcao') # Não funciona
# CORRETO: patch no local onde é usado
import modulo_externo
@patch('modulo_externo.funcao') # Funciona
8. Exemplo Completo: Testando um Serviço com Dependências Externas
import requests
import sqlite3
from unittest.mock import patch, MagicMock
class ServicoClima:
def __init__(self, api_key, db_path):
self.api_key = api_key
self.db_path = db_path
def salvar_clima(self, cidade):
# Dependência externa 1: API de clima
response = requests.get(
f'https://api.climatempo.com.br/{cidade}',
headers={'Authorization': f'Bearer {self.api_key}'}
)
if response.status_code != 200:
raise ValueError("Erro ao buscar clima")
dados_clima = response.json()
# Dependência externa 2: Banco de dados
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
"INSERT INTO climas (cidade, temperatura, umidade) VALUES (?, ?, ?)",
(cidade, dados_clima['temperatura'], dados_clima['umidade'])
)
conn.commit()
conn.close()
return dados_clima
@patch('requests.get')
@patch('sqlite3.connect')
def test_salvar_clima_sucesso(mock_connect, mock_get):
# Configurar mock da API
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"temperatura": 25.5,
"umidade": 60
}
mock_get.return_value = mock_response
# Configurar mock do banco
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_connect.return_value = mock_conn
mock_conn.cursor.return_value = mock_cursor
# Executar teste
servico = ServicoClima("api_key_teste", ":memory:")
resultado = servico.salvar_clima("São Paulo")
# Verificações
assert resultado["temperatura"] == 25.5
assert resultado["umidade"] == 60
mock_get.assert_called_once_with(
'https://api.climatempo.com.br/São Paulo',
headers={'Authorization': 'Bearer api_key_teste'}
)
mock_cursor.execute.assert_called_once_with(
"INSERT INTO climas (cidade, temperatura, umidade) VALUES (?, ?, ?)",
("São Paulo", 25.5, 60)
)
mock_conn.commit.assert_called_once()
@patch('requests.get')
def test_salvar_clima_erro_api(mock_get):
mock_response = MagicMock()
mock_response.status_code = 500
mock_get.return_value = mock_response
servico = ServicoClima("api_key_teste", ":memory:")
import pytest
with pytest.raises(ValueError, match="Erro ao buscar clima"):
servico.salvar_clima("Rio de Janeiro")
Este exemplo demonstra como mockar duas dependências externas simultaneamente, garantindo que o teste seja rápido, determinístico e isolado.
Referências
- unittest.mock — mock object library - Documentação Oficial Python — Documentação completa da biblioteca
unittest.mockcom exemplos e referência de API - Python Mocking: A Comprehensive Guide - Real Python — Guia abrangente sobre mocking em Python com exemplos práticos e casos de uso
- Understanding Python Mock Objects - TestDriven.io — Tutorial detalhado sobre objetos mock, patch e boas práticas
- Mock vs MagicMock - Python Documentation — Diferenças entre Mock e MagicMock na documentação oficial
- Pytest Mocking Techniques - Pytest Documentation — Guia oficial do pytest sobre técnicas de mocking e monkeypatch
- Python Mocking Best Practices - ArjanCodes — Artigo sobre boas práticas e armadilhas comuns ao usar mocks em Python