Bem-vindo à aula sobre mocks em Python! Nesta aula, vamos explorar uma das ferramentas mais poderosas para testes: a criação de objetos simulados (mocks) que substituem dependências reais do código sob teste. Isso permite testar unidades de código de forma isolada, sem depender de serviços externos, bancos de dados, APIs ou até mesmo de classes complexas internas.

Mocks são essenciais para escrever testes rápidos, determinísticos e focados. Ao simular o comportamento de dependências, você pode verificar se o código interage corretamente com elas, sem se preocupar com a implementação real. Nesta aula, vamos mergulhar no módulo unittest.mock, na função patch e em boas práticas para usar mocks de forma eficaz.

unittest.mock

O módulo unittest.mock foi introduzido no Python 3.3 e fornece classes e utilitários para criar objetos mock. A classe principal é Mock, que cria objetos que podem ter atributos e métodos definidos dinamicamente. O MagicMock é uma subclasse que suporta métodos mágicos (como __len__, __iter__, etc.), tornando-o mais versátil para simular objetos complexos.

Um objeto mock registra chamadas feitas a ele, permitindo que você faça asserções sobre essas chamadas. Por exemplo, você pode verificar se um método foi chamado com certos argumentos, quantas vezes foi chamado, ou até mesmo definir o valor de retorno. Isso é extremamente útil para testar a interação entre componentes.

Vejamos um exemplo simples de criação e uso de um mock:

from unittest.mock import Mock

# Cria um mock
m = Mock()

# Define um método com retorno específico
m.calcular.return_value = 42

# Chama o método
resultado = m.calcular(10, 20)

# Verifica chamadas
m.calcular.assert_called_once_with(10, 20)
print(resultado)  # 42

Nesse exemplo, criamos um mock, definimos o retorno do método calcular e depois verificamos que ele foi chamado com os argumentos corretos. Isso é uma demonstração básica, mas mostra o poder dos mocks.

patch

A função patch é usada para substituir temporariamente um objeto em um módulo ou classe por um mock. Ela pode ser usada como decorador ou como context manager. O alvo do patch pode ser uma string no formato 'pacote.modulo.objeto' ou um objeto. O patch substitui o objeto durante a execução do bloco ou da função decorada e o restaura após o término.

Existem três formas comuns de usar o patch:

  • Como decorador de função: @patch('modulo.funcao')
  • Como context manager: with patch('modulo.funcao') as mock_funcao:
  • Com patch.object para métodos de uma classe.

Vamos ver um exemplo de uso de patch para substituir uma função externa:

# arquivo: meu_modulo.py
def buscar_dados():
    # Simula uma chamada a uma API
    return {"dados": [1, 2, 3]}

def processar():
    dados = buscar_dados()
    return sum(dados["dados"])

No teste, podemos usar patch para substituir buscar_dados por um mock que retorna dados controlados:

from unittest.mock import patch
from meu_modulo import processar

with patch('meu_modulo.buscar_dados') as mock_buscar:
    mock_buscar.return_value = {"dados": [10, 20, 30]}
    resultado = processar()
    assert resultado == 60

O patch substitui a função no módulo meu_modulo pelo mock, e o mock retorna o valor definido. Assim, podemos testar processar sem depender da implementação real de buscar_dados.

Mockando dependências

Muitas vezes, o código depende de serviços externos, como APIs, bancos de dados ou classes de infraestrutura. Mockar essas dependências permite testar a lógica de negócio isoladamente, sem precisar de acesso a esses serviços. Isso torna os testes mais rápidos e confiáveis, pois não dependem de rede ou de estado externo.

Por exemplo, considere uma classe que envia emails:

class EmailService:
    def enviar(self, destinatario, mensagem):
        # Simula envio real
        return True

class Notificador:
    def __init__(self, email_service):
        self.email_service = email_service

    def notificar(self, usuario, mensagem):
        self.email_service.enviar(usuario.email, mensagem)

Para testar Notificador, podemos criar um mock de EmailService e verificar se o método enviar é chamado corretamente:

from unittest.mock import MagicMock

class Usuario:
    def __init__(self, email):
        self.email = email

email_service = MagicMock()
notificador = Notificador(email_service)
usuario = Usuario("user@example.com")
notificador.notificar(usuario, "Olá!")

email_service.enviar.assert_called_once_with("user@example.com", "Olá!")

Nesse exemplo, usamos MagicMock para criar um mock de EmailService e verificamos que o método enviar foi chamado com os argumentos esperados. Isso nos permite testar a lógica de Notificador sem enviar emails reais.

Outra técnica comum é usar patch para substituir uma classe inteira ou um objeto importado. Por exemplo, se um módulo importa uma classe de outro módulo, podemos usar patch para substituir essa classe por um mock que simula seu comportamento.

# arquivo: servico.py
from conexao import Conexao

class Servico:
    def __init__(self):
        self.conexao = Conexao()

    def realizar_operacao(self):
        return self.conexao.executar("SELECT * FROM tabela")

No teste, podemos patch a classe Conexao para retornar dados falsos:

from unittest.mock import patch

with patch('servico.Conexao') as MockConexao:
    instancia = MockConexao.return_value
    instancia.executar.return_value = [{"id": 1}]
    servico = Servico()
    resultado = servico.realizar_operacao()
    assert resultado == [{"id": 1}]

Nesse exemplo, patch substitui a classe Conexao dentro do módulo servico por um mock. Quando Servico é instanciado, ele usa o mock da classe Conexao, e o método executar retorna o valor definido.

Boas práticas

Usar mocks de forma eficaz requer algumas boas práticas para evitar testes frágeis e que não reflitam a realidade. Aqui estão algumas recomendações:

  • Mocke apenas o que é externo: Não crie mocks para código interno que você pode testar diretamente. Mocks são para dependências que você não controla, como APIs, bancos de dados, ou classes de terceiros.
  • Prefira patch a criar mocks manualmente: A função patch substitui objetos no local onde são usados, o que é mais realista do que injetar mocks manualmente.
  • Use MagicMock quando precisar de métodos mágicos: MagicMock suporta operações como __len__, __iter__, etc., úteis para simular objetos que serão usados em contextos específicos.
  • Verifique as chamadas com precisão: Use assert_called_once_with ou assert_any_call para garantir que as interações estão corretas.
  • Evite mocks excessivos: Se você está mockando muitas coisas, pode estar testando apenas a interação entre mocks, não a lógica real. Tente manter o teste focado.
  • Use spec para limitar atributos: Ao criar um mock, você pode especificar spec=ClasseReal para que o mock apenas aceite atributos que existem na classe real, evitando erros de digitação.
from unittest.mock import MagicMock

class MinhaClasse:
    def metodo_real(self):
        pass

mock = MagicMock(spec=MinhaClasse)
mock.metodo_real()  # OK
mock.metodo_inexistente()  # AttributeError

Além disso, é importante lembrar que mocks são ferramentas para testes, não para produção. Não use mocks no código de produção. Eles devem ser usados apenas nos testes para simular dependências.

Boas práticas adicionais

Uma boa prática é usar mocks para testar o comportamento de funções que dependem de tempo, como time.sleep ou datetime.now. Isso permite que você controle o tempo no teste, tornando-o determinístico.

from unittest.mock import patch
from datetime import datetime

def obter_ano_atual():
    return datetime.now().year

with patch('datetime.datetime') as mock_datetime:
    mock_datetime.now.return_value = datetime(2020, 1, 1)
    assert obter_ano_atual() == 2020

Outra prática é usar mocks para simular erros e exceções, garantindo que o código lida com falhas corretamente.

from unittest.mock import patch
import requests

def buscar_usuario(user_id):
    response = requests.get(f"https://api.exemplo.com/users/{user_id}")
    response.raise_for_status()
    return response.json()

with patch('requests.get') as mock_get:
    mock_get.side_effect = requests.exceptions.RequestException("Erro de rede")
    try:
        buscar_usuario(1)
    except requests.exceptions.RequestException:
        print("Erro capturado")

Isso mostra como testar a robustez do código diante de falhas externas.

Referências

Exercícios

  1. Exercício 1: Crie um mock simples de uma função que retorna a soma de dois números. Verifique se a função foi chamada com os argumentos corretos e se o retorno é o esperado.

    ✓ Resposta:
    from unittest.mock import Mock
    
    # Cria mock
    mock_soma = Mock()
    mock_soma.return_value = 5
    
    # Chama
    resultado = mock_soma(2, 3)
    
    # Verifica
    mock_soma.assert_called_once_with(2, 3)
    assert resultado == 5
    print(resultado)  # 5
  2. Exercício 2: Usando patch, substitua a função time.time para retornar um valor fixo e teste uma função que usa o tempo atual.

    ✓ Resposta:
    import time
    from unittest.mock import patch
    
    def obter_timestamp():
        return int(time.time())
    
    with patch('time.time', return_value=1600000000):
        assert obter_timestamp() == 1600000000
    print("Teste passou!")
  3. Exercício 3: Crie uma classe que dependa de um serviço de banco de dados. Use MagicMock para simular o serviço e verifique se o método de salvar é chamado corretamente.

    ✓ Resposta:
    from unittest.mock import MagicMock
    
    class BancoDeDados:
        def salvar(self, dados):
            pass
    
    class Repositorio:
        def __init__(self, db):
            self.db = db
    
        def salvar_usuario(self, usuario):
            self.db.salvar(usuario)
    
    db_mock = MagicMock()
    repo = Repositorio(db_mock)
    usuario = {"nome": "João"}
    repo.salvar_usuario(usuario)
    db_mock.salvar.assert_called_once_with(usuario)
    print("Teste passou!")
  4. Exercício 4: Use patch para substituir uma classe externa em um módulo e teste uma função que a utiliza. Verifique se o método da classe mockada foi chamado.

    ✓ Resposta:
    # mod.py
    class ClienteExterno:
        def obter_dados(self):
            return "dados reais"
    
    def processar():
        cliente = ClienteExterno()
        return cliente.obter_dados()
    
    # teste
    from unittest.mock import patch
    
    with patch('mod.ClienteExterno') as MockCliente:
        instancia = MockCliente.return_value
        instancia.obter_dados.return_value = "dados mockados"
        resultado = processar()
        assert resultado == "dados mockados"
        instancia.obter_dados.assert_called_once_with()
    print("Teste passou!")
  5. Exercício 5: Escreva um teste que use side_effect para simular uma exceção em uma chamada de API e verifique se a função lida com o erro adequadamente.

    ✓ Resposta:
    import requests
    from unittest.mock import patch
    
    def buscar_dados(url):
        try:
            response = requests.get(url)
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException:
            return None
    
    with patch('requests.get') as mock_get:
        mock_get.side_effect = requests.exceptions.RequestException("Erro")
        resultado = buscar_dados("https://api.exemplo.com")
        assert resultado is None
        mock_get.assert_called_once_with("https://api.exemplo.com")
    print("Teste passou!")