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