Mocks são objetos simulados que imitam o comportamento de objetos reais de forma controlada. Em testes de unidade, eles são essenciais para isolar o código sob teste, substituindo dependências como chamadas de API, acesso a banco de dados ou operações de I/O. Isso permite verificar se o código interage corretamente com essas dependências, sem depender de sua implementação real.

Nesta aula, vamos explorar o módulo unittest.mock da biblioteca padrão do Python, que fornece ferramentas poderosas para criar e gerenciar mocks. Você aprenderá a usar a função patch para substituir objetos em módulos, a configurar retornos e efeitos colaterais, e a verificar chamadas. Também discutiremos boas práticas para evitar armadilhas comuns.

unittest.mock

O módulo unittest.mock é uma biblioteca de mocking integrada ao Python desde a versão 3.3. Ele fornece classes como Mock, MagicMock, patch, entre outras, para criar objetos falsos que podem ser usados em testes. A classe Mock é a base: ela aceita qualquer chamada de atributo ou método e retorna outro Mock por padrão, permitindo que você configure comportamentos específicos.

Um dos recursos mais úteis é a capacidade de configurar atributos e métodos de retorno. Por exemplo, você pode definir que um método de um mock retorne um valor específico quando chamado. Além disso, mocks registram todas as chamadas feitas a eles, permitindo que você faça asserções sobre como o código interage com a dependência.

Vamos ver um exemplo básico:

from unittest.mock import Mock

# Cria um mock
m = Mock()

# Configura o retorno de um método
m.some_method.return_value = 42

# Chama o método
result = m.some_method()

# Verifica o resultado
print(result)  # Saída: 42

# Verifica se o método foi chamado
m.some_method.assert_called_once()

Além de Mock, existe MagicMock, que fornece implementações padrão de métodos mágicos (como __len__, __iter__, etc.). Isso é útil quando o objeto mockado precisa suportar protocolos especiais, como iteração ou acesso a itens.

Outra funcionalidade importante é a capacidade de configurar efeitos colaterais com side_effect. Você pode passar uma função que será chamada em vez do retorno, permitindo simular exceções ou comportamentos dinâmicos.

from unittest.mock import Mock

m = Mock()
m.method.side_effect = ValueError("Erro simulado")

try:
    m.method()
except ValueError as e:
    print(e)  # Saída: Erro simulado

patch

A função patch é usada para substituir temporariamente objetos em módulos ou classes durante um teste. Ela pode ser usada como decorador ou como gerenciador de contexto. O alvo do patch é uma string que identifica o objeto a ser substituído, no formato 'pacote.modulo.objeto'.

Quando o teste termina, o patch é revertido automaticamente, restaurando o objeto original. Isso garante que os testes não interfiram uns nos outros.

Exemplo com decorador:

from unittest.mock import patch

@patch('meu_modulo.minha_funcao')
def test_com_patch(mock_funcao):
    mock_funcao.return_value = 10
    # Código que usa minha_funcao
    from meu_modulo import usa_funcao
    resultado = usa_funcao()
    assert resultado == 10

Exemplo com gerenciador de contexto:

from unittest.mock import patch

with patch('meu_modulo.minha_funcao') as mock_funcao:
    mock_funcao.return_value = 10
    # Código que usa minha_funcao
    from meu_modulo import usa_funcao
    resultado = usa_funcao()
    assert resultado == 10

O patch também pode ser usado para substituir classes inteiras, ou para criar mocks automáticos de objetos. Por exemplo, patch('requests.get') substitui a função get do módulo requests, permitindo simular respostas HTTP sem fazer chamadas reais.

Mockando dependências

Em muitos casos, o código que você testa depende de serviços externos, como APIs, bancos de dados ou arquivos. Mockar essas dependências permite que você teste a lógica do seu código de forma isolada, garantindo que os testes sejam rápidos e confiáveis.

Vamos considerar um exemplo prático: uma função que faz uma requisição HTTP e processa a resposta. Em vez de fazer uma chamada real, usamos patch para simular a resposta.

import requests

def obter_dados(api_url):
    resposta = requests.get(api_url)
    if resposta.status_code == 200:
        return resposta.json()
    else:
        raise Exception("Erro na requisição")

Para testar essa função sem acessar a internet, patcheamos requests.get:

from unittest.mock import patch, Mock
import minha_api

def test_obter_dados_sucesso():
    mock_resposta = Mock()
    mock_resposta.status_code = 200
    mock_resposta.json.return_value = {"chave": "valor"}

    with patch('requests.get', return_value=mock_resposta) as mock_get:
        resultado = minha_api.obter_dados('http://exemplo.com/api')
        assert resultado == {"chave": "valor"}
        mock_get.assert_called_once_with('http://exemplo.com/api')

Outro cenário comum é mockar uma classe de banco de dados. Suponha que você tenha uma classe Repositorio com métodos para salvar e recuperar dados. Em testes, você pode substituir a classe por um mock e verificar se os métodos são chamados corretamente.

from unittest.mock import patch

def salvar_usuario(repositorio, usuario):
    repositorio.salvar(usuario)

# No teste
with patch('meu_modulo.Repositorio') as MockRepositorio:
    repositorio = MockRepositorio()
    salvar_usuario(repositorio, {"nome": "Ana"})
    repositorio.salvar.assert_called_once_with({"nome": "Ana"})

É importante destacar que, ao mockar dependências, você deve garantir que o comportamento simulado seja realista o suficiente para que o teste seja significativo. Por exemplo, se você está testando um tratamento de erro, deve configurar o mock para lançar uma exceção.

Boas práticas

O uso de mocks deve ser feito com moderação e critério. Aqui estão algumas boas práticas para escrever testes eficazes:

  • Mocke apenas o que é necessário: Não mocke tudo indiscriminadamente. Foque nas dependências externas que tornam o teste lento, não determinístico ou difícil de configurar.
  • Prefira mocks a monkey patching manual: O módulo unittest.mock fornece ferramentas seguras e gerenciadas para substituir objetos, evitando problemas de escopo e restauração.
  • Use MagicMock quando precisar de métodos mágicos: Se o objeto mockado for usado em operações como len() ou iteração, MagicMock fornece implementações padrão.
  • Verifique chamadas com asserções: Use assert_called_once_with, assert_any_call, etc., para garantir que o código interage corretamente com a dependência.
  • Evite mockar detalhes internos: Mocke a interface pública, não implementações internas. Isso torna os testes mais robustos a mudanças.
  • Mantenha os testes legíveis: Configure mocks de forma clara e organizada, usando helpers se necessário.
  • Considere usar patch como decorador para simplificar: Reduz a indentação e torna o teste mais direto.
  • Não teste a implementação do mock em si: Lembre-se de que você está testando o seu código, não o mock.

Além disso, é importante entender a diferença entre mocks, stubs e fakes. Mocks são usados para verificar interações, stubs para fornecer respostas prontas e fakes são implementações simplificadas que funcionam. Em testes de unidade, muitas vezes usamos stubs (configurando retornos) e mocks (verificando chamadas) juntos.

Referências

Exercícios

  1. Crie um mock de uma função chamada get_data que retorne um dicionário qualquer. Verifique se a função foi chamada exatamente uma vez e com o argumento "chave".

    ✓ Resposta:
    from unittest.mock import Mock
    
    mock = Mock(return_value={"dado": 1})
    resultado = mock("chave")
    assert resultado == {"dado": 1}
    mock.assert_called_once_with("chave")
    
  2. Utilize patch para substituir a função os.getcwd em um teste e faça com que retorne "/tmp". Verifique se a função foi chamada.

    ✓ Resposta:
    from unittest.mock import patch
    
    with patch('os.getcwd', return_value='/tmp') as mock_getcwd:
        import os
        caminho = os.getcwd()
        assert caminho == '/tmp'
        mock_getcwd.assert_called_once()
    
  3. Escreva um teste para a função soma que recebe dois números e os soma. Use um mock para simular uma dependência externa que não existe (por exemplo, um módulo chamado calculadora).

    ✓ Resposta:
    from unittest.mock import patch
    
    def soma(a, b):
        from calculadora import somar
        return somar(a, b)
    
    with patch('calculadora.somar', return_value=5) as mock_somar:
        resultado = soma(2, 3)
        assert resultado == 5
        mock_somar.assert_called_once_with(2, 3)
    
  4. Crie um mock de uma classe BancoDeDados com um método conectar que levanta uma exceção. Teste que o código que usa essa classe trata a exceção corretamente.

    ✓ Resposta:
    from unittest.mock import Mock
    
    class BancoDeDados:
        def conectar(self):
            pass
    
    def conectar_com_tratamento():
        try:
            db = BancoDeDados()
            db.conectar()
            return "conectado"
        except Exception:
            return "erro"
    
    mock_db = Mock()
    mock_db.conectar.side_effect = Exception("Falha")
    with patch('__main__.BancoDeDados', return_value=mock_db):
        resultado = conectar_com_tratamento()
        assert resultado == "erro"
    
  5. Usando patch como decorador, teste uma função que chama requests.get e retorna o conteúdo JSON. Simule uma resposta com status 200 e dados específicos.

    ✓ Resposta:
    import requests
    from unittest.mock import patch, Mock
    
    def obter_json(url):
        resposta = requests.get(url)
        return resposta.json()
    
    @patch('requests.get')
    def test_obter_json(mock_get):
        mock_resposta = Mock()
        mock_resposta.status_code = 200
        mock_resposta.json.return_value = {"mensagem": "ok"}
        mock_get.return_value = mock_resposta
    
        resultado = obter_json('http://exemplo.com')
        assert resultado == {"mensagem": "ok"}
        mock_get.assert_called_once_with('http://exemplo.com')
    

Observações finais

Mocks são ferramentas poderosas para testes, mas devem ser usados com discernimento. Lembre-se de que o objetivo é testar o comportamento do seu código, não o mock em si. Sempre que possível, prefira testes de integração para validar a interação real com dependências, reservando mocks para testes de unidade que exigem isolamento.

Além disso, a partir do Python 3.3, unittest.mock é a biblioteca padrão, mas existem alternativas como pytest-mock que integram mocks de forma mais elegante com o framework pytest. Vale a pena explorar essas opções em projetos reais.