Mock em Python: patch, side_effect e AsyncMock
Teste código Python sem chamar APIs reais: unittest.mock, patch no lugar certo, autospec, side_effect e AsyncMock, com exemplos executáveis em pytest.
Para testar Python sem chamar APIs reais, use unittest.mock: crie um substituto com create_autospec, configure return_value ou side_effect e confira as chamadas. Quando a dependência já foi importada pelo módulo, aplique patch onde ela é usada, não necessariamente onde foi definida. Para funções assíncronas, use AsyncMock e verificações de await.
Isso permite testar uma consulta de CEP, um envio de e-mail ou uma integração de CRM sem rede, cobrança ou mensagens reais. O objetivo não é simular todo o programa: é isolar a fronteira externa e continuar executando a sua regra de negócio.
Neste guia, os exemplos usam Python 3.10 ou superior e pytest. O mock vem da biblioteca padrão; não é necessário instalar pytest-mock. Se testes ainda são novidade, comece pelo guia de pytest.
Mock, MagicMock, patch ou monkeypatch?
| Recurso | Quando usar | Cuidado principal |
|---|---|---|
Mock | Substituir uma função ou objeto e registrar chamadas | Sem especificação, aceita atributos inexistentes |
MagicMock | Simular objetos com métodos especiais, como gerenciadores de contexto | Não deixar comportamento implícito esconder erros |
create_autospec | Criar substituto com interface e assinatura conhecidas | Não valida os tipos das anotações |
patch | Substituir temporariamente uma referência do módulo | Escolher o namespace correto |
AsyncMock | Simular uma função aguardada com await | Ser chamado não significa ser aguardado |
monkeypatch do pytest | Trocar variáveis de ambiente, atributos ou diretório | Não oferece sozinho as verificações de chamadas de um mock |
patch é o mecanismo de substituição; Mock é um tipo de objeto substituto. Eles se complementam. Você também pode injetar uma dependência como argumento e passar um mock, sem aplicar patch em nenhum módulo.
Projeto executável: consultar um CEP sem rede nos testes
Crie uma pasta com três arquivos:
exemplo-mock/
├── clientes.py
├── pedidos.py
└── test_pedidos.py
Dentro de um ambiente virtual, instale o executor:
python -m pip install pytest
No terminal Linux/macOS, o comando pode ser python3; no Windows, pode ser py. Use sempre o interpretador do ambiente do projeto. Um ModuleNotFoundError geralmente indica que a instalação e a execução usaram ambientes diferentes.
1. A fronteira externa: clientes.py
Este cliente usa apenas a biblioteca padrão e faz uma consulta ao ViaCEP. Os testes vão substituir a função inteira, portanto não executam esta chamada HTTP:
# clientes.py
import json
from urllib.error import URLError
from urllib.request import urlopen
def buscar_cep(cep: str) -> dict:
if len(cep) != 8 or not cep.isascii() or not cep.isdigit():
raise ValueError("CEP deve conter oito dígitos ASCII")
try:
with urlopen(
f"https://viacep.com.br/ws/{cep}/json/", timeout=5
) as resposta:
dados = json.load(resposta)
except (URLError, TimeoutError) as exc:
raise ConnectionError("Serviço de CEP indisponível") from exc
if dados.get("erro"):
raise ValueError("CEP não encontrado")
return dados
O timeout evita uma espera indefinida. Este é um cliente didático: uma aplicação real também precisa decidir como tratar JSON inválido e respostas fora do contrato. Para normalização, validação e diferenças entre provedores, veja CPF, CNPJ e CEP com BrasilAPI e ViaCEP.
2. A regra de negócio: pedidos.py
O módulo transforma a resposta externa em um resumo e define uma resposta para indisponibilidade:
# pedidos.py
from clientes import buscar_cep
def resumir_destino(cep: str) -> str:
try:
dados = buscar_cep(cep)
except ConnectionError:
return "Consulta temporariamente indisponível"
return f"{dados['localidade']}/{dados['uf']}"
O código deixa ValueError propagar: entrada inválida não é a mesma coisa que falha de rede. O teste precisa preservar essa distinção, em vez de assumir que todo erro deve ser escondido.
3. Teste de sucesso com patch e autospec
Salve em test_pedidos.py:
# test_pedidos.py
from unittest.mock import patch
import pytest
from pedidos import resumir_destino
def test_resumo_do_destino():
with patch("pedidos.buscar_cep", autospec=True) as consulta:
consulta.return_value = {
"localidade": "São Paulo",
"uf": "SP",
}
resultado = resumir_destino("01001000")
assert resultado == "São Paulo/SP"
consulta.assert_called_once_with("01001000")
Rode, na pasta dos arquivos:
python -m pytest -q
O resultado esperado neste ponto é um teste aprovado. Não é necessário ter internet para executá-lo. O with restaura a referência original ao terminar, inclusive se uma asserção falhar.
Por que patch em pedidos.buscar_cep, e não em clientes.buscar_cep?
from clientes import buscar_cep cria uma referência no namespace de pedidos. Trocar depois clientes.buscar_cep não troca automaticamente a referência já importada em pedidos.
A regra é substituir o nome que o código consulta:
| Importação no módulo sob teste | Chamada realizada | Alvo típico de patch |
|---|---|---|
from clientes import buscar_cep em pedidos.py | buscar_cep(cep) | pedidos.buscar_cep |
import clientes em pedidos.py | clientes.buscar_cep(cep) | pedidos.clientes.buscar_cep |
No segundo caso, o atributo pertence ao mesmo objeto de módulo clientes; substituir clientes.buscar_cep também alcança essa chamada. O primeiro caso é a armadilha mais comum. Se o teste ainda acessa a rede, confira o import e o alvo antes de suspeitar do pytest.
side_effect: falhas e resultados sucessivos
Acrescente estes testes ao mesmo test_pedidos.py. O pytest e o patch já foram importados no início do arquivo.
# Acrescente a test_pedidos.py
def test_servico_indisponivel():
with patch("pedidos.buscar_cep", autospec=True) as consulta:
consulta.side_effect = ConnectionError("timeout simulado")
assert resumir_destino("01001000") == (
"Consulta temporariamente indisponível"
)
consulta.assert_called_once_with("01001000")
def test_cep_invalido_nao_e_ocultado():
with patch("pedidos.buscar_cep", autospec=True) as consulta:
consulta.side_effect = ValueError("CEP inválido")
with pytest.raises(ValueError, match="CEP inválido"):
resumir_destino("abc")
def test_duas_consultas_com_resultados_diferentes():
with patch("pedidos.buscar_cep", autospec=True) as consulta:
consulta.side_effect = [
ConnectionError("primeira consulta falhou"),
{"localidade": "Recife", "uf": "PE"},
]
assert resumir_destino("50030000") == (
"Consulta temporariamente indisponível"
)
assert resumir_destino("50030000") == "Recife/PE"
assert consulta.call_count == 2
A lista produz um item por chamada. Quando o item é uma exceção, ela é levantada; quando é um valor, ele é retornado. Ao esgotar a lista, ocorre StopIteration.
Esse último teste não comprova retry automático: quem chama duas vezes é o próprio teste. Para testar uma política de retry, execute a função que contém essa política uma vez e verifique suas tentativas, limites e condições. Não simule atrasos reais com sleep na suíte rápida; injete ou substitua a função de espera.
Injeção de dependência com create_autospec
Se você controla o desenho da aplicação, receber o cliente como argumento reduz a dependência de caminhos de import. Este exemplo independente mostra o padrão:
from unittest.mock import create_autospec
def enviar_email(destinatario: str, assunto: str) -> None:
raise NotImplementedError("Adaptador externo fora deste exemplo")
def avisar_candidato(email: str, enviar) -> None:
enviar(email, "Nova vaga Python")
substituto = create_autospec(enviar_email, spec_set=True)
avisar_candidato("[email protected]", substituto)
substituto.assert_called_once_with(
"[email protected]", "Nova vaga Python"
)
O endereço é fictício e nenhum e-mail é enviado. O adaptador real pode ser passado na inicialização da aplicação; nos testes, passe o substituto.
autospec ajuda a detectar chamadas incompatíveis com a assinatura. spec_set restringe a criação de atributos que não existem na especificação. Esses recursos não conferem tipos como str, não validam a forma do dicionário retornado e não garantem compatibilidade com a API remota. Configure os dados simulados de acordo com o contrato real.
Um Mock sem especificação pode aceitar um método digitado errado e deixar o teste passar. Por isso, prefira uma interface pequena e conhecida a um objeto genérico com dezenas de atributos inventados.
AsyncMock: verificar chamadas aguardadas
Para código async/await, uma chamada cria um objeto aguardável. É possível chamar a função sem executar await, o que seria um bug. Por isso, a verificação adequada é assert_awaited_once_with:
import asyncio
from unittest.mock import AsyncMock
async def consultar_status(cliente, codigo: str) -> str:
dados = await cliente(codigo)
return dados["status"]
async def demonstrar():
cliente = AsyncMock(return_value={"status": "processando"})
resultado = await consultar_status(cliente, "pedido-123")
assert resultado == "processando"
cliente.assert_awaited_once_with("pedido-123")
asyncio.run(demonstrar())
Esse exemplo roda como script, sem plugin assíncrono do pytest. Em uma aplicação que já tem um event loop ativo, não chame asyncio.run dentro dele: use await demonstrar() no fluxo assíncrono existente. Ao criar mocks para clientes reais, use a especificação da interface em vez de copiar o objeto genérico de demonstração sem adaptação.
O que não deve ser mockado?
Prefira manter reais as transformações locais, cálculos, validações e estruturas de dados baratas. Se toda função interna é substituída, o teste apenas confirma o roteiro que você escreveu para os mocks.
Para arquivos temporários, a fixture tmp_path do pytest geralmente é mais útil que simular cada chamada de open. Para configurações, use monkeypatch.setenv e monkeypatch.delenv, como no guia de variáveis de ambiente. Para vários cenários da mesma regra, combine os testes com pytest.mark.parametrize.
Separe três responsabilidades:
- Teste unitário da regra: substitui rede, envio e outros efeitos externos; verifica resultados e interações relevantes.
- Teste do adaptador: verifica parsing, status HTTP, timeouts e tradução de erros, usando transporte ou servidor controlado.
- Teste de integração: confirma o contrato com sandbox ou serviço permitido, separado da suíte rápida e sem dados pessoais reais.
Os testes de resumir_destino não cobrem o corpo de buscar_cep. Isso é intencional, mas precisa estar explícito: um mock bem configurado não prova que o endpoint continua disponível nem que o cliente trata todos os formatos de resposta.
Checklist para mocks confiáveis
- O alvo de
patché o nome consultado pelo módulo? - O teste executa a regra real, e não uma versão mockada dela?
- A resposta simulada representa um contrato plausível?
- Há cenários de sucesso, indisponibilidade e entrada inválida?
- As verificações de chamadas são relevantes ao comportamento, e não detalhes internos frágeis?
autospecprotege a assinatura quando aplicável?- Toda substituição é restaurada por contexto, fixture ou decorator?
- Testes assíncronos conferem o
await? - Existe cobertura separada para o adaptador e suas integrações?
Perguntas frequentes
Preciso instalar unittest.mock?
Não. Ele acompanha Python. Instale pytest para executar os testes deste guia. O plugin pytest-mock oferece a fixture mocker, mas é uma opção de conveniência, não uma exigência.
Por que meu patch não impede a chamada real?
Normalmente o patch foi aplicado no módulo de origem, enquanto o código usa uma referência importada em outro namespace. Inspecione a linha de import e substitua o nome usado no módulo sob teste.
return_value ou side_effect?
Use return_value para um retorno constante. Use side_effect para exceções, retornos calculados ou sequências de respostas. Uma sequência exige que você controle quantas chamadas são esperadas.
Mock ou monkeypatch?
monkeypatch é conveniente para mudanças temporárias no ambiente e em atributos durante testes pytest. unittest.mock adiciona objetos com histórico de chamadas e comportamento configurável. É possível usar ambos no mesmo projeto.
Posso confiar só nos mocks para validar uma integração?
Não. Eles testam seu comportamento diante de uma simulação. Verifique o adaptador e mantenha testes de integração apropriados, sem transformar a suíte unitária em um consumidor de serviços reais.
Próximo passo
Execute os quatro testes do projeto e altere temporariamente o argumento da consulta em pedidos.py: a verificação assert_called_once_with deve detectar a mudança. Depois, escreva testes próprios para o adaptador, cobrindo uma resposta inválida e um CEP não encontrado.
Para um projeto de portfólio, documente quais testes rodam offline e quais dependem de sandbox. Essa distinção comunica maturidade de engenharia melhor que uma porcentagem de cobertura isolada; veja também a trilha de QA e automação de testes com Python.