Como usar mocking seletivo para testar integrações sem dependências reais
1. Fundamentos do Mocking Seletivo em Testes de Integração
O mocking seletivo é uma técnica de teste onde substituímos apenas partes específicas de dependências externas, mantendo o restante do sistema funcionando com implementações reais. Diferente do mocking total, que substitui todas as dependências por objetos falsos, o mocking seletivo preserva a lógica de negócio e apenas isola pontos de integração instáveis, lentos ou caros.
Cenários ideais para mocking seletivo incluem:
- APIs de terceiros com limites de requisição ou custos por chamada
- Serviços de pagamento que exigem transações reais
- Filas de mensagens em ambientes de teste com latência imprevisível
- Bancos de dados que precisam de estado consistente mas são lentos em operações específicas
É crucial entender a diferença entre mocks (objetos que verificam chamadas), stubs (objetos que retornam respostas fixas) e fakes (implementações simplificadas funcionais). No mocking seletivo, usamos predominantemente mocks configuráveis com comportamento específico.
2. Identificando os Pontos de Integração para Mocking
O primeiro passo é mapear todas as dependências externas do sistema: APIs REST, chamadas a bancos de dados, serviços de mensageria, gateways de pagamento, etc. Para cada dependência, avalie:
- Confiabilidade: o serviço cai com frequência?
- Custo: cada chamada tem custo financeiro?
- Velocidade: a resposta leva mais de 500ms?
- Determinismo: o comportamento é previsível?
A regra de ouro é: mockar o que é instável ou caro, manter real o que é lógica de negócio. Por exemplo, ao testar um sistema de e-commerce, mocke o gateway de pagamento (instável e caro), mas mantenha real a validação de estoque no banco de dados.
Exemplo prático — separação entre chamada HTTP e validação de resposta:
# Código real: serviço que consulta preço em API externa
class PrecoService:
def obter_preco(self, produto_id):
resposta = requests.get(f"https://api.exemplo.com/precos/{produto_id}")
if resposta.status_code == 200:
dados = resposta.json()
return self._aplicar_desconto(dados["preco"])
raise Exception("Falha ao obter preço")
def _aplicar_desconto(self, preco):
return preco * 0.9
Aqui, mockamos apenas requests.get, mas testamos _aplicar_desconto com dados reais.
3. Estruturação de Mocks Seletivos com Frameworks Populares
Em Python, o módulo unittest.mock oferece ferramentas poderosas. Use patch seletivo para substituir apenas a função de requisição HTTP:
from unittest.mock import patch, MagicMock
import pytest
@patch("servico.requests.get")
def test_obter_preco_com_desconto(mock_get):
# Configura o mock para retornar uma resposta específica
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"preco": 100.0}
mock_get.return_value = mock_response
servico = PrecoService()
resultado = servico.obter_preco(123)
assert resultado == 90.0 # 100 - 10% de desconto
mock_get.assert_called_once_with("https://api.exemplo.com/precos/123")
Com pytest-mock, a sintaxe fica mais limpa:
def test_obter_preco_falha(mocker):
mock_get = mocker.patch("servico.requests.get")
mock_get.side_effect = requests.exceptions.Timeout("Timeout")
servico = PrecoService()
with pytest.raises(Exception, match="Falha ao obter preço"):
servico.obter_preco(123)
Boas práticas: evite mocks genéricos que retornam None. Prefira mocks com comportamento específico que simulam respostas reais.
4. Simulação de Respostas Parciais e Estados de Erro
O mocking seletivo brilha ao simular cenários de erro sem depender de serviços externos. Use side_effect para criar sequências de respostas:
def test_processar_pagamento_com_retry(mocker):
mock_pagamento = mocker.patch("servico.PagamentoGateway.cobrar")
# Simula: falha na primeira tentativa, sucesso na segunda
mock_pagamento.side_effect = [
{"status": "erro", "codigo": 500},
{"status": "sucesso", "transacao_id": "ABC123"}
]
resultado = processar_pagamento(100.0)
assert resultado["transacao_id"] == "ABC123"
assert mock_pagamento.call_count == 2
Exemplo mais completo: mockar apenas o serviço de pagamento, mantendo o banco de dados real:
def test_criar_pedido_com_pagamento(mocker, db_session):
# Mock apenas do gateway de pagamento
mock_cobrar = mocker.patch("servico.PagamentoGateway.cobrar")
mock_cobrar.return_value = {"status": "sucesso", "transacao_id": "TXN001"}
# Banco de dados real
usuario = db_session.query(Usuario).first()
pedido = PedidoService.criar(usuario.id, [{"produto_id": 1, "quantidade": 2}])
assert pedido.status == "pago"
assert pedido.transacao_id == "TXN001"
# Verifica que o banco foi realmente atualizado
assert db_session.query(Pedido).count() == 1
5. Estratégias para Validação de Interações com Mocks
Validar as interações com mocks é essencial para garantir que a integração ocorreu corretamente:
def test_enviar_email_confirmacao(mocker):
mock_email = mocker.patch("servico.EmailService.enviar")
processar_pedido(123, "usuario@exemplo.com")
# Verifica argumentos específicos
mock_email.assert_called_once_with(
destinatario="usuario@exemplo.com",
assunto="Pedido #123 confirmado",
template="confirmacao_pedido"
)
# Verifica contagem de chamadas
assert mock_email.call_count == 1
Use spy para monitorar chamadas sem substituir o comportamento real:
def test_log_chamada_api(mocker):
# Spy no método real, mas ainda o executa
spy = mocker.spy(servico, "enviar_para_api")
resultado = servico.processar_dados({"chave": "valor"})
spy.assert_called_once()
# O método real ainda foi executado
assert resultado["processado"] == True
6. Lidando com Dependências Aninhadas e Mocks em Cadeia
Quando uma dependência chama outra, isole apenas o ponto crítico:
# Serviço A -> Serviço B -> API externa
class ServicoA:
def executar(self):
resultado = ServicoB().processar()
return self._formatar(resultado)
def test_servico_a_com_mock_em_cadeia(mocker):
# Mock apenas na chamada HTTP final
mock_requests = mocker.patch("servico_b.requests.get")
mock_requests.return_value.json.return_value = {"dados": "valor"}
servico_a = ServicoA()
resultado = servico_a.executar()
assert "valor" in resultado
# ServicoB foi executado real, apenas a API foi mockada
Evite mocks em excesso: se você mockar ServicoA, ServicoB e a API, estará testando o mock, não a integração real.
7. Integração Contínua e Manutenção de Mocks Seletivos
Em pipelines de CI, versione mocks e fixtures reutilizáveis:
# conftest.py
@pytest.fixture
def mock_pagamento(mocker):
mock = mocker.patch("servico.PagamentoGateway.cobrar")
mock.return_value = {"status": "sucesso"}
return mock
@pytest.fixture
def mock_api_externa(mocker):
mock = mocker.patch("servico.requests.get")
mock.return_value.status_code = 200
mock.return_value.json.return_value = {"preco": 50.0}
return mock
Monitore mudanças em APIs externas com testes de contrato:
def test_contrato_api_externa():
# Teste real que roda apenas em ambiente de staging
if os.getenv("ENV") == "staging":
resposta = requests.get("https://api.exemplo.com/health")
assert resposta.status_code == 200
8. Armadilhas Comuns e Como Evitá-las
Armadilha 1: Mockar objetos internos que mascaram bugs.
- Solução: nunca mocke métodos da própria classe sendo testada.
Armadilha 2: Dependência excessiva de mocks que tornam os testes frágeis.
- Solução: se um teste quebra por mudança na implementação interna, o mock é específico demais.
Armadilha 3: Mocks que não refletem o comportamento real da API.
- Solução: use dados reais de exemplos da documentação da API.
Estratégias de refatoração:
- Substitua mocks por testes de contrato para validar schemas
- Use containers de teste (Testcontainers) para dependências como bancos e filas
- Prefira mocks de interface (portas) em vez de mocks de implementação
Referências
- Documentação oficial unittest.mock — Guia completo do módulo de mocking do Python, incluindo patch, MagicMock e side_effect
- pytest-mock: plugin para mocking em testes — Documentação oficial com exemplos de uso do mocker fixture
- Martin Fowler: Mocks Aren't Stubs — Artigo clássico explicando as diferenças entre mocks, stubs e fakes
- Testcontainers para Python — Biblioteca para gerenciar containers Docker em testes de integração
- Real Python: Mocking in Python — Tutorial prático sobre mocking com exemplos do mundo real
- Arquitetura Hexagonal e Testes de Integração — Abordagem para projetar sistemas que facilitam mocking seletivo
- Documentação pytest: fixtures e mocking — Guia oficial sobre monkeypatch e fixtures para testes