<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"https://api.exemplo.com/usuarios/{usuario_id}")
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('servico.requests.get') as mock_get:
Configurar o comportamento do mock
mock_resposta = MagicMock()
mock_resposta.json.return_value = {'id': 1, 'nome': 'João'}
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['nome'] == 'João'
mock_get.assert_called_once_with('https://api.exemplo.com/usuarios/1')</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 = {'id': 1, 'nome': 'Maria'}
resultado = mock_banco.buscar_usuario(1)
assert resultado == {'id': 1, 'nome': 'Maria'}
O mock retorna o mesmo valor toda vez que é chamado
resultado2 = mock_banco.buscar_usuario(999)
assert resultado2 == {'id': 1, 'nome': 'Maria'}</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("Conexão recusada")
with pytest.raises(requests.ConnectionError):
mock_requisicao.get("https://api.exemplo.com")
def test_side_effect_multiplos_retornos():
mock_iterador = MagicMock()
Retornar valores diferentes em cada chamada
mock_iterador.processar.side_effect = [1, 2, 3, Exception("Fim")]
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('smtp.gmail.com', 587)
servidor.sendmail('seu@email.com', 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('smtplib.SMTP'): # Isso não vai funcionar!
servico = EnviadorEmail()
resultado = servico.enviar('user@example.com', 'Olá')
def test_envio_email_correto():
CORRETO: fazendo patch onde é usado
with patch('email_service.smtplib.SMTP') as mock_smtp:
mock_smtp.return_value.sendmail.return_value = None
servico = EnviadorEmail()
resultado = servico.enviar('user@example.com', 'Olá')
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('datetime.datetime') 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('modulo.servico_externo.fazer_requisicao')
def test_com_decorator(mock_requisicao):
mock_requisicao.return_value = {'status': 'sucesso'}
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('modulo.banco_dados')
@patch('modulo.cache')
@patch('modulo.servico_api')
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('processador.requests.get')
Configurar o retorno é mais direto
mock_get.return_value.json.return_value = {'id': 1, 'nome': 'João'}
Executar e validar
servico = ProcessadorDados()
resultado = servico.buscar_usuario(1)
assert resultado['nome'] == 'João'
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):
"""Fixture que fornece um mock já configurado para o OAuth"""
mock = mocker.patch('autenticacao.requests.post')
mock.return_value.json.return_value = {
'access_token': 'token_valido_123',
'expires_in': 3600
}
return mock
def test_login_sucesso(mock_oauth_provider):
autenticador = AutenticadorOAuth()
token = autenticador.fazer_login('usuario', 'senha')
assert token == 'token_valido_123'
mock_oauth_provider.assert_called_once()
def test_login_com_refresh_token(mock_oauth_provider):
O mesmo mock é reutilizado
autenticador = AutenticadorOAuth()
autenticador.fazer_login('usuario', 'senha')
Configurar novo comportamento para próxima chamada
mock_oauth_provider.return_value.json.return_value = {
'access_token': 'novo_token_456',
'expires_in': 3600
}
novo_token = autenticador.renovar_token('token_valido_123')
assert novo_token == 'novo_token_456'</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 "espioná-la" 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, 'somar')
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, 'r') 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 = "linha1\nlinha2\nlinha3\n"
mocker.patch('builtins.open', mock_open(read_data=mock_file_data))
processador = ProcessadorCSV()
resultado = processador.processar('dados.csv')
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"{self.url_base}/{endpoint}")
resposta.raise_for_status()
return resposta.json()
except requests.RequestException as e:
return {'erro': str(e), 'status': 'falha'}
test_api_service.py
def test_tratamento_erro_conexao(mocker):
import requests
mock_get = mocker.patch('requests.get')
mock_get.side_effect = requests.ConnectionError("Servidor indisponível")
cliente = ClienteAPI("https://api.exemplo.com")
resultado = cliente.buscar_dados("users")
assert resultado['status'] == 'falha'
assert 'indisponível' in resultado['erro']</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, 'a')
mock_obj.metodo(2, 'b')
mock_obj.metodo(1, 'a') # 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, 'a'),
call(2, 'b'),
call(1, 'a')
])
Verificar que foi chamado com argumentos específicos em algum momento
mock_obj.metodo.assert_any_call(2, 'b')
Obter o último argumento com que foi chamado
assert mock_obj.metodo.call_args == call(1, 'a')</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'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><!-- FIM --></p>