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.

12 Oct 2026 9 min de leitura Equipe Python Dev BR

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?

RecursoQuando usarCuidado principal
MockSubstituir uma função ou objeto e registrar chamadasSem especificação, aceita atributos inexistentes
MagicMockSimular objetos com métodos especiais, como gerenciadores de contextoNão deixar comportamento implícito esconder erros
create_autospecCriar substituto com interface e assinatura conhecidasNão valida os tipos das anotações
patchSubstituir temporariamente uma referência do móduloEscolher o namespace correto
AsyncMockSimular uma função aguardada com awaitSer chamado não significa ser aguardado
monkeypatch do pytestTrocar variáveis de ambiente, atributos ou diretórioNã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 testeChamada realizadaAlvo típico de patch
from clientes import buscar_cep em pedidos.pybuscar_cep(cep)pedidos.buscar_cep
import clientes em pedidos.pyclientes.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:

  1. Teste unitário da regra: substitui rede, envio e outros efeitos externos; verifica resultados e interações relevantes.
  2. Teste do adaptador: verifica parsing, status HTTP, timeouts e tradução de erros, usando transporte ou servidor controlado.
  3. 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?
  • autospec protege 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.

Referências

E
Equipe Python Dev BR

Contribuidor do Python Dev BR

Artigos relacionados