Python

Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado

16 min de leitura

Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado

Introdução: Por que Mocks são Essenciais Quando desenvolvemos software profissional, frequentemente precisamos testar código que depende de recursos externos: APIs, bancos de dados, sistemas de arquivo, ou serviços terceirizados. O problema é que essas dependências tornam os testes lentos, instáveis e difíceis de manter. É aqui que mocks entram em cena. Um mock é um objeto que simula o comportamento de um objeto real, permitindo que você controle completamente como ele se comporta dentro de um teste. Em vez de fazer uma chamada real a uma API, por exemplo, você substitui aquele serviço por um mock que retorna exatamente o que você precisa, quando você precisa. Isso não apenas torna seus testes mais rápidos e confiáveis, mas também permite testar cenários que seriam impossíveis com dependências reais (como simular falhas de rede). Python oferece várias ferramentas para trabalhar com mocks: é a biblioteca padrão da linguagem, é o mecanismo de substituição e é um plugin que torna a experiência ainda

<h2>Introdução: Por que Mocks são Essenciais</h2>

<p>Quando desenvolvemos software profissional, frequentemente precisamos testar código que depende de recursos externos: APIs, bancos de dados, sistemas de arquivo, ou serviços terceirizados. O problema é que essas dependências tornam os testes lentos, instáveis e difíceis de manter. É aqui que <strong>mocks</strong> entram em cena.</p>

<p>Um mock é um objeto que simula o comportamento de um objeto real, permitindo que você controle completamente como ele se comporta dentro de um teste. Em vez de fazer uma chamada real a uma API, por exemplo, você substitui aquele serviço por um mock que retorna exatamente o que você precisa, quando você precisa. Isso não apenas torna seus testes mais rápidos e confiáveis, mas também permite testar cenários que seriam impossíveis com dependências reais (como simular falhas de rede).</p>

<p>Python oferece várias ferramentas para trabalhar com mocks: <code>unittest.mock</code> é a biblioteca padrão da linguagem, <code>patch</code> é o mecanismo de substituição e <code>pytest-mock</code> é um plugin que torna a experiência ainda mais intuitiva. Dominar essas ferramentas é fundamental para escrever testes profissionais e manuteníveis.</p>

<h2>unittest.mock: Fundamentos e Conceitos Centrais</h2>

<h3>O que é Mock e MagicMock</h3>

<p>A biblioteca <code>unittest.mock</code> oferece dois tipos principais de objetos simulados: <code>Mock</code> e <code>MagicMock</code>. A diferença fundamental é que <code>MagicMock</code> implementa automaticamente métodos mágicos (dunder methods) como <code>__str__</code>, <code>__len__</code>, <code>__iter__</code>, tornando-o mais versátil. Na maioria dos casos reais, você usará <code>MagicMock</code>.</p>

<p>Veja um exemplo prático. Imagine que você tem uma classe que faz requisições HTTP:</p>

<pre><code class="language-python"># servico.py

import requests

class ProcessadorDados:

def buscar_usuario(self, usuario_id):

resposta = requests.get(f&quot;https://api.exemplo.com/usuarios/{usuario_id}&quot;)

return resposta.json()</code></pre>

<p>Testar isso de verdade significa fazer uma requisição real, o que é lento e frágil. Com mocks, você faz isso:</p>

<pre><code class="language-python"># test_servico.py

from unittest.mock import MagicMock, patch

import pytest

from servico import ProcessadorDados

def test_buscar_usuario_com_mock():

Criar um mock para requests

with patch(&#039;servico.requests.get&#039;) as mock_get:

Configurar o comportamento do mock

mock_resposta = MagicMock()

mock_resposta.json.return_value = {&#039;id&#039;: 1, &#039;nome&#039;: &#039;João&#039;}

mock_get.return_value = mock_resposta

Executar o código que será testado

servico = ProcessadorDados()

resultado = servico.buscar_usuario(1)

Fazer asserções

assert resultado[&#039;nome&#039;] == &#039;João&#039;

mock_get.assert_called_once_with(&#039;https://api.exemplo.com/usuarios/1&#039;)</code></pre>

<p>Neste exemplo, usamos <code>patch</code> para substituir <code>requests.get</code> por um mock, configuramos o retorno esperado, e depois verificamos que a função foi chamada corretamente. O teste executa em milissegundos sem fazer nenhuma requisição real.</p>

<h3>Configurando Comportamentos: return_value e side_effect</h3>

<p>Um mock é inútil se você não conseguir controlar seu comportamento. Os dois mecanismos principais são <code>return_value</code> (o que o mock retorna quando chamado) e <code>side_effect</code> (efeitos colaterais, como exceções ou sequências de retornos).</p>

<p>Use <code>return_value</code> quando o mock deve sempre retornar a mesma coisa:</p>

<pre><code class="language-python">from unittest.mock import MagicMock

def test_configurar_return_value():

mock_banco = MagicMock()

mock_banco.buscar_usuario.return_value = {&#039;id&#039;: 1, &#039;nome&#039;: &#039;Maria&#039;}

resultado = mock_banco.buscar_usuario(1)

assert resultado == {&#039;id&#039;: 1, &#039;nome&#039;: &#039;Maria&#039;}

O mock retorna o mesmo valor toda vez que é chamado

resultado2 = mock_banco.buscar_usuario(999)

assert resultado2 == {&#039;id&#039;: 1, &#039;nome&#039;: &#039;Maria&#039;}</code></pre>

<p>Use <code>side_effect</code> quando precisa simular comportamentos mais complexos, como lançar exceções ou retornar valores diferentes em cada chamada:</p>

<pre><code class="language-python">from unittest.mock import MagicMock

import requests

def test_side_effect_excecao():

mock_requisicao = MagicMock()

mock_requisicao.get.side_effect = requests.ConnectionError(&quot;Conexão recusada&quot;)

with pytest.raises(requests.ConnectionError):

mock_requisicao.get(&quot;https://api.exemplo.com&quot;)

def test_side_effect_multiplos_retornos():

mock_iterador = MagicMock()

Retornar valores diferentes em cada chamada

mock_iterador.processar.side_effect = [1, 2, 3, Exception(&quot;Fim&quot;)]

assert mock_iterador.processar() == 1

assert mock_iterador.processar() == 2

assert mock_iterador.processar() == 3

with pytest.raises(Exception):

mock_iterador.processar()</code></pre>

<h2>patch: Substituindo Objetos no Local Correto</h2>

<h3>Entendendo o Escopo de patch</h3>

<p>O conceito mais importante ao usar <code>patch</code> é <strong>onde</strong> você está substituindo o objeto. Muitos iniciantes comettem o erro de fazer patch no lugar errado e acabam gastando horas investigando por que o mock não funciona.</p>

<p>A regra de ouro é: <strong>faça patch onde o objeto é usado, não onde é definido</strong>. Se você tem um módulo <code>autenticacao.py</code> que importa <code>requests</code>, você não faz patch em <code>requests</code>, mas sim em <code>autenticacao.requests</code>.</p>

<p>Considere este exemplo real:</p>

<pre><code class="language-python"># email_service.py

import smtplib

class EnviadorEmail:

def enviar(self, destinatario, mensagem):

Usando smtplib diretamente

servidor = smtplib.SMTP(&#039;smtp.gmail.com&#039;, 587)

servidor.sendmail(&#039;seu@email.com&#039;, destinatario, mensagem)

return True

test_email_service.py

from unittest.mock import patch

from email_service import EnviadorEmail

def test_envio_email_errado():

ERRADO: fazendo patch no lugar errado

with patch(&#039;smtplib.SMTP&#039;): # Isso não vai funcionar!

servico = EnviadorEmail()

resultado = servico.enviar(&#039;user@example.com&#039;, &#039;Olá&#039;)

def test_envio_email_correto():

CORRETO: fazendo patch onde é usado

with patch(&#039;email_service.smtplib.SMTP&#039;) as mock_smtp:

mock_smtp.return_value.sendmail.return_value = None

servico = EnviadorEmail()

resultado = servico.enviar(&#039;user@example.com&#039;, &#039;Olá&#039;)

assert resultado == True

Verificar que sendmail foi chamado com os argumentos corretos

mock_smtp.return_value.sendmail.assert_called_once()</code></pre>

<h3>Patch como Context Manager e Decorator</h3>

<p>Existem duas formas principais de usar <code>patch</code>: como context manager (com <code>with</code>) ou como decorator (com <code>@patch</code>). Cada uma tem seu uso apropriado.</p>

<p>Use <strong>context manager</strong> quando você precisa fazer setup/teardown dentro de um teste específico ou quando quer ativar o patch apenas para uma parte do teste:</p>

<pre><code class="language-python">from unittest.mock import patch

from datetime import datetime

def test_com_context_manager():

Apenas dentro deste bloco, datetime.now será substituído

with patch(&#039;datetime.datetime&#039;) as mock_datetime:

mock_datetime.now.return_value = datetime(2024, 1, 1, 12, 0, 0)

Seu código aqui usa o mock

resultado = funcao_que_usa_datetime()

assert resultado == alguma_coisa

Fora do contexto, datetime volta ao normal</code></pre>

<p>Use <strong>decorator</strong> quando você quer que o patch seja aplicado a todo o teste. O mock é passado como argumento à função de teste:</p>

<pre><code class="language-python">from unittest.mock import patch

@patch(&#039;modulo.servico_externo.fazer_requisicao&#039;)

def test_com_decorator(mock_requisicao):

mock_requisicao.return_value = {&#039;status&#039;: &#039;sucesso&#039;}

resultado = minha_funcao_que_chama_servico()

assert resultado == esperado</code></pre>

<p>Múltiplos decorators são aplicados de baixo para cima (reversamente), e aparecem nos argumentos na mesma ordem invertida:</p>

<pre><code class="language-python">@patch(&#039;modulo.banco_dados&#039;)

@patch(&#039;modulo.cache&#039;)

@patch(&#039;modulo.servico_api&#039;)

def test_multiplos_patches(mock_api, mock_cache, mock_banco):

mock_api é de servico_api

mock_cache é de cache

mock_banco é de banco_dados

pass</code></pre>

<h2>pytest-mock: Simplificando o Trabalho com Fixtures</h2>

<h3>Introdução ao Plugin pytest-mock</h3>

<p>Enquanto <code>unittest.mock</code> é poderoso, <code>pytest-mock</code> é uma camada que torna a experiência muito mais pythônica e menos verbosa. Em vez de usar decorators e context managers, você recebe um fixture chamado <code>mocker</code> que oferece métodos convenientes. Para usar, instale com <code>pip install pytest-mock</code>.</p>

<p>A principal vantagem do <code>pytest-mock</code> é que você não precisa se preocupar com cleanup: pytest cuida disso automaticamente quando o teste termina. Além disso, a sintaxe é mais intuitiva:</p>

<pre><code class="language-python"># test_com_pytest_mock.py

from processador import ProcessadorDados

def test_buscar_usuario_pytest_mock(mocker):

Em vez de patch como context manager ou decorator,

você simplesmente chama mocker.patch()

mock_get = mocker.patch(&#039;processador.requests.get&#039;)

Configurar o retorno é mais direto

mock_get.return_value.json.return_value = {&#039;id&#039;: 1, &#039;nome&#039;: &#039;João&#039;}

Executar e validar

servico = ProcessadorDados()

resultado = servico.buscar_usuario(1)

assert resultado[&#039;nome&#039;] == &#039;João&#039;

mock_get.assert_called_once()</code></pre>

<h3>Fixtures e Mocking: Um Exemplo Realista</h3>

<p>A verdadeira força do <code>pytest-mock</code> aparece quando você combina com fixtures. Imagine um projeto real onde múltiplos testes precisam de mocks similares:</p>

<pre><code class="language-python"># test_autenticacao.py

import pytest

from autenticacao import AutenticadorOAuth

@pytest.fixture

def mock_oauth_provider(mocker):

&quot;&quot;&quot;Fixture que fornece um mock já configurado para o OAuth&quot;&quot;&quot;

mock = mocker.patch(&#039;autenticacao.requests.post&#039;)

mock.return_value.json.return_value = {

&#039;access_token&#039;: &#039;token_valido_123&#039;,

&#039;expires_in&#039;: 3600

}

return mock

def test_login_sucesso(mock_oauth_provider):

autenticador = AutenticadorOAuth()

token = autenticador.fazer_login(&#039;usuario&#039;, &#039;senha&#039;)

assert token == &#039;token_valido_123&#039;

mock_oauth_provider.assert_called_once()

def test_login_com_refresh_token(mock_oauth_provider):

O mesmo mock é reutilizado

autenticador = AutenticadorOAuth()

autenticador.fazer_login(&#039;usuario&#039;, &#039;senha&#039;)

Configurar novo comportamento para próxima chamada

mock_oauth_provider.return_value.json.return_value = {

&#039;access_token&#039;: &#039;novo_token_456&#039;,

&#039;expires_in&#039;: 3600

}

novo_token = autenticador.renovar_token(&#039;token_valido_123&#039;)

assert novo_token == &#039;novo_token_456&#039;</code></pre>

<p>Neste padrão, você define uma fixture que cria um mock com configurações padrão, e depois múltiplos testes podem usar essa fixture, economizando código e tornando os testes mais legíveis.</p>

<h2>Casos de Uso Avançados e Boas Práticas</h2>

<h3>Spy: Testando Chamadas sem Substituir Completamente</h3>

<p>Às vezes você não quer substituir completamente uma função, mas sim &quot;espioná-la&quot; para verificar como foi chamada. O <code>unittest.mock</code> oferece <code>wraps</code> para isso, mas <code>pytest-mock</code> oferece um método mais conveniente: <code>mocker.spy()</code>.</p>

<pre><code class="language-python"># calculadora.py

class Calculadora:

def somar(self, a, b):

return a + b

def calcular_total(self, valores):

total = 0

for valor in valores:

total = self.somar(total, valor)

return total

test_calculadora.py

def test_spy_chamadas(mocker):

calc = Calculadora()

Criar um spy: monitora chamadas mas deixa o código rodar normalmente

spy_somar = mocker.spy(calc, &#039;somar&#039;)

resultado = calc.calcular_total([1, 2, 3])

assert resultado == 6

Verificar que somar foi chamado 3 vezes

assert spy_somar.call_count == 3

Verificar as chamadas em detalhe

assert spy_somar.call_args_list[0] == mocker.call(0, 1)

assert spy_somar.call_args_list[1] == mocker.call(1, 2)

assert spy_somar.call_args_list[2] == mocker.call(3, 3)</code></pre>

<h3>Testando Arquivos e Sistema de Arquivos Sem Criar Arquivos Reais</h3>

<p>Um caso comum é testar código que lê ou escreve arquivos. Você não quer criar arquivos reais durante testes. Aqui, <code>mocker.patch</code> com <code>mock_open</code> (da biblioteca <code>unittest.mock</code>) é ideal:</p>

<pre><code class="language-python"># processador_arquivo.py

class ProcessadorCSV:

def processar(self, caminho_arquivo):

with open(caminho_arquivo, &#039;r&#039;) as f:

linhas = f.readlines()

return len(linhas)

test_processador_arquivo.py

from unittest.mock import mock_open

def test_processar_arquivo(mocker):

Simular arquivo com 3 linhas

mock_file_data = &quot;linha1\nlinha2\nlinha3\n&quot;

mocker.patch(&#039;builtins.open&#039;, mock_open(read_data=mock_file_data))

processador = ProcessadorCSV()

resultado = processador.processar(&#039;dados.csv&#039;)

assert resultado == 3</code></pre>

<h3>Testando Exceções e Casos de Erro</h3>

<p>Um dos maiores valores de mocks é a capacidade de testar casos de erro sem gerar erros reais. Use <code>side_effect</code> com exceções:</p>

<pre><code class="language-python"># api_service.py

class ClienteAPI:

def __init__(self, url_base):

self.url_base = url_base

def buscar_dados(self, endpoint):

import requests

try:

resposta = requests.get(f&quot;{self.url_base}/{endpoint}&quot;)

resposta.raise_for_status()

return resposta.json()

except requests.RequestException as e:

return {&#039;erro&#039;: str(e), &#039;status&#039;: &#039;falha&#039;}

test_api_service.py

def test_tratamento_erro_conexao(mocker):

import requests

mock_get = mocker.patch(&#039;requests.get&#039;)

mock_get.side_effect = requests.ConnectionError(&quot;Servidor indisponível&quot;)

cliente = ClienteAPI(&quot;https://api.exemplo.com&quot;)

resultado = cliente.buscar_dados(&quot;users&quot;)

assert resultado[&#039;status&#039;] == &#039;falha&#039;

assert &#039;indisponível&#039; in resultado[&#039;erro&#039;]</code></pre>

<h3>Asserções Avançadas em Mocks</h3>

<p>Além de verificar se um mock foi chamado, você pode fazer asserções sofisticadas sobre como foi chamado:</p>

<pre><code class="language-python">from unittest.mock import call

def test_asercoes_avancadas(mocker):

mock_obj = mocker.MagicMock()

Simular várias chamadas

mock_obj.metodo(1, &#039;a&#039;)

mock_obj.metodo(2, &#039;b&#039;)

mock_obj.metodo(1, &#039;a&#039;) # Chamada repetida

Verificar número total de chamadas

assert mock_obj.metodo.call_count == 3

Verificar sequência exata de chamadas

mock_obj.metodo.assert_has_calls([

call(1, &#039;a&#039;),

call(2, &#039;b&#039;),

call(1, &#039;a&#039;)

])

Verificar que foi chamado com argumentos específicos em algum momento

mock_obj.metodo.assert_any_call(2, &#039;b&#039;)

Obter o último argumento com que foi chamado

assert mock_obj.metodo.call_args == call(1, &#039;a&#039;)</code></pre>

<h2>Conclusão</h2>

<p>Após trabalhar profissionalmente com testes por anos, posso dizer com segurança que <strong>mocks são a base de testes rápidos e confiáveis</strong>. O aprendizado principal é este: mocks não são truques avançados, são necessidade prática. Sem eles, você fica preso a testes lentos que dependem de sistemas externos, o que cria um ciclo vicioso onde a suite de testes fica tão lenta que você para de executá-la.</p>

<p>O segundo ponto essencial é entender que <code>patch</code> funciona melhor quando você aplica a regra de ouro: patch no lugar onde o objeto é usado, não onde é definido. 99% dos problemas que vejo com mocks em código legado são por aplicar patch no lugar errado.</p>

<p>Finalmente, use <code>pytest-mock</code> em novos projetos. Sim, <code>unittest.mock</code> é padrão, mas <code>pytest-mock</code> é superior em experiência do desenvolvedor e integração com pytest. A economia de linhas de código e a clareza dos testes compensa largamente a dependência de um plugin externo.</p>

<h2>Referências</h2>

<ol>

<li><a href="https://docs.python.org/3/library/unittest.mock.html" target="_blank" rel="noopener noreferrer">unittest.mock — documentação oficial Python</a></li>

<li><a href="https://pytest-mock.readthedocs.io/" target="_blank" rel="noopener noreferrer">pytest-mock — documentação do plugin</a></li>

<li><a href="https://realpython.com/python-mock-library/" target="_blank" rel="noopener noreferrer">Real Python — Getting Started With Mocking in Python</a></li>

<li><a href="https://martinfowler.com/articles/mocksArentStubs.html" target="_blank" rel="noopener noreferrer">Martin Fowler — Mocks Aren&#039;t Stubs</a></li>

<li><a href="https://pragprog.com/titles/bopytest/python-testing-with-pytest/" target="_blank" rel="noopener noreferrer">Python Testing with pytest — Brian Okken, Cap. 7</a></li>

</ol>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Guia Completo de Context Managers em Python: with, __enter__, __exit__ e contextlib
Guia Completo de Context Managers em Python: with, __enter__, __exit__ e contextlib

O que são Context Managers? Context managers são um padrão de design em Pytho...

Ruff, Black e isort em Python: Linting e Formatação Automatizada: Do Básico ao Avançado
Ruff, Black e isort em Python: Linting e Formatação Automatizada: Do Básico ao Avançado

Introdução: Por que Qualidade de Código Importa Quando você começa a programa...

Dominando Celery em Python: Filas de Tarefas, Workers e Beat Scheduler em Projetos Reais
Dominando Celery em Python: Filas de Tarefas, Workers e Beat Scheduler em Projetos Reais

O que é Celery e por que você precisa disso Celery é uma biblioteca Python as...