Mocking: dublando partes do código


É esperado que o nosso código se conecte com outras coisas, além dele próprio. Essa conexão pode ser com um banco de dados, uma API externa ou até mesmo um arquivo. Nem sempre conseguimos testar o nosso código sem essas dependências externas. Às vezes é uma questão de economia: se temos um código que se conecta com a API do Google Maps, por exemplo, todas as vezes que rodamos os nossos testes será feita uma requisição para essa API. Além de usar a internet para fazer a conexão, o teste ficará mais lento (pois estará fazendo uma requisição real oficial) e irá consumir um recurso que deveria ser gasto apenas com uma aplicação real.

Por isso existem os Mocks. Mockar um trecho de código significa criar um dublê pra ele; isso mesmo, um dublê. Esse dublê irá interpretar, atuar, como o código real mas sem chamá-lo exatamente.

Essa técnica é bastante útil para testar funcionalidades que envolvem banco de dados, serviços externos e até mesmo conexões com o sistema operacional. Vamos lá?

O freela (de novo)

No texto Nos testes nós confiamos - TDD com Python nós implementamos um módulo que classifica o gênero de uma pessoa, baseado apenas no primeiro nome. Para isso, utilizamos a API Genderize.io.

Aqui temos o nosso código de produção. Vejamos o que ele faz:

def find_by(name):
    if name == '' or name is None:
        raise EmptyName('The name is empty!')

    first_name = _extract_first_name(name)
    result = _search_on_api(first_name)

    data = {
        'first_name': first_name,
        'gender': result['gender'] if result['gender'] else 'unidentified'
    }
    return data

def _search_on_api(name):
    return requests.get(GENDERIZE_ENDPOINT.format(name)).json()

def _extract_first_name(name):
    return name.split(' ')[0]

Resumindo: recebe um nome completo, extrai o primeiro nome e busca na API. O método _search_on_api(name) é responsável por fazer um request e retornar um JSON.

Aqui os nossos principais testes:

def test_should_return_female_when_first_name_is_from_female():
    expected_result = {
        'name':'Ana',
        'gender':'female',
        'probability':1,
        'count':23
    }
    result = gender.find_by('Ana Ferreira')

    assert result['gender'] == 'female'


def test_should_return_male_when_first_name_is_from_male():
    expected_result = {
        'name':'Mateus',
        'gender':'male',
        'probability':1,
        'count':23
    }
    result = gender.find_by('Mateus Costa')

    assert result['gender'] == 'male'

Nossos testes estão cobrindo os fluxos principais e estão passando. Mas temos um problema: todas as vezes que executamos os testes, a API Genderize.io (que é externa) é chamada! O que leva os nossos testes a demorarem alguns segundos para serem executados (numa máquina modesta). Além disso, nós temos um limite de 1000 requisições por dia.

Podemos evitar que a API externa seja chamada, deixando os testes mais rápido e economizando nosso limite de requisições mockando.

O que precisamos mockar?

Hum… Polêmico.

Para você ter noção do quanto essa pergunta divide as pessoas, quero lhe contar (caso você não saiba ainda), que existem duas escolas de estilos de Test-Driven Development (TDD): a escola de London e a escola de Chicago. A galera de London segue o chamado mockist-style, que acredita que as dependências devem ser sempre mockadas e o teste deve verificar se o mock dessa dependência foi chamado ou não. A escola de Chicago, conhecida como Classic TDD, acredita que as chamadas devem ser feitas de maneira real oficialⓇ. Claro, essas definições foram feitas de maneira simplista mas você pode ler um pouco mais sobre isso no texto Classic TDD or “London School”?.

O livro [Growing Object Oriented Software Guided By Tests](), bem útil para entender os testes em todo o processo de criação do software, segue uma linha no estilo London School. O chato é só gravar o nome dele. 😅

Mas voltando a nossa pergunta… Pra mim, depende, sempre. Não acredito que seja possível adotar um único estilo sempre. A minha “regra” pessoal é: dependências externas, como interações com o sistema operacional ou uma API, eu mocko. O que é código interno, onde eu tenho o poder de modificar, eu não mocko. O motivo é simples: quando a gente mocka as nossas dependências internas, o nosso código, não temos a oportunidade de exercitar aquele fluxo. No caso das dependências externas, não temos controle sobre aquele código, só temos a saída que esperamos dele.

Claro, nada é escrito em pedra. Às vezes existem situações onde é preciso mockar partes complexas do seu próprio código que não tem ligação com dependências externas e isso sim pode ser um code smell. Se está complexo demais, significa que o design precisa ser melhorado. O uso de mocks em si eu não acredito que seja um smell, afinal não é possível testar de maneira rápida e econômica sem o uso deles.

Mockando com Python

Já sabemos que temos que mockar a nossa requisição para a API. Como fazemos isso?

Precisamos fazer com que toda a vez que chamarmos a API externa, uma estrutura substitua essa chamada e aja como se fosse ela. No Python, temos a biblioteca mock que faz todo o trabalho sujo. Para importá-la, use from unittest import mock. Você pode dar uma olhada na documentação dela depois.

A biblioteca te dá diversas opções de como mockar. Para esse caso, vamos utilizar o patch, útil para substituir um objeto/módulo durante um teste. Podemos usá-lo através do decorator @patch, passando como parâmetro o caminho do módulo/método a ser mockado. ATENÇÃO: esse caminho precisa ser o caminho dentro da sua aplicação. Vejamos:

from unittest import mock


@mock.patch('detector.gender.requests.get')
def test_should_return_female_when_first_name_is_from_female(mock_requests):
    expected_result = {
        'name':'Ana',
        'gender':'female',
        'probability':1,
        'count':23
    }
    mock_requests.return_value.json.return_value = expected_result

    result = gender.find_by('Ana Ferreira')

    mock_requests.assert_called_once()
    assert result['gender'] == 'female'

O mock será o método get, da biblioteca requests. Nosso caminho ficará assim: detector.gender.requests.get. A lista de argumentos do nosso teste ficará de acordo com os patchs que você fizer. Você pode acumular diversos; a ordem da lista de argumentos ficará da esquerda pra direita de acordo com os patchs que esteja de baixo pra cima. Meio confuso mas com o tempo você acostuma. Ou não. 😂

Mockar uma estrutura nos dá várias possibilidades. Podemos apenas verificar se o mock foi chamado, com quais argumentos foi chamados e também retornar um valor. Nesse exemplo, usamos o return_value. Estamos dizendo pro nosso mock que, quando ele for chamado, ele deve retornar algo.

No caso do mock_requests.return_value.json.return_value = expected_result, quando chamamos o get vários atributos são retornados. Eles podem ser status_code, headers, text… Mas nós só estamos usando o json. Por isso estamos configurando o nosso mock para quando ele chamar o método get retornar um método json e então retornar o expected_result, que contém um dicionário, mesmo formato retornado pelo método json() real.

Ao chamar o método que estamos testando, ele irá chamar o nosso mock. Dessa forma, o nosso teste verifica como o código que construímos se comporta ao invés de testar se a API funciona ou não (o que não é nossa responsabilidade).

Depois que mockamos, os nossos dois testes foram de 1.34 segundos para 0.09 segundos de execução. Nada mal!

Mais exemplos

Existem diversas situações onde você pode usar mocks e de diferentes maneiras. Você pode usar o MagicMock, onde o mock terá os magic methods implementados, ou o atributo side_effect para simular uma exceção ou um outro comportamento inesperado. Aqui deixo alguns outros exemplos para referência:

Mockando o método os.walk - exemplo completo

@pytest.fixture()
def os_walk_mock(mocker):
    directory = '/tests/fixtures'
    mocker.patch('os.getcwd', return_value='/home/bla')
    yield mocker.patch('os.walk', return_value=[(directory, [], ['foo.py'])])

Mockando a abertura de um arquivo read() - exemplo completo

def test_should_return_method_with_one_parameter():
    file_mock = mock.mock_open(read_data='def foo(bar):')

    with mock.patch('builtins.open', file_mock, create=True):
        methods_list = extractor.all_methods()

Além disso, existem vários tipos de asserts que você pode utilizar, por exemplo:

  • assert_called
  • assert_called_once_with
  • assert_has_calls
  • assert_not_called
  • call_count

O exemplo completo do freela você pode ver aqui. Nesse exemplo usamos o pytest para executar os testes e Python 3.6.1.

Por hoje é só, pessoal

Falamos bastante sobre as vantagens de mockar e eu acho que a esse ponto vocês também podem reconhecer os benefícios! Entretanto, é preciso tomar cuidado. Às vezes os mocks podem mascarar alguns resultados inesperados. Por isso sempre tente conhecer o que pode vir do outro lado pra antecipar os cenários nos seus testes.

Espero que o textão ajude algum de vocês em algum momento. Bons testes!

Outras referências:

Mocks Aren’t Stubs por Martin Fowler

Aprendendo TDD! Mais sobre dublês de teste, com exemplos em Ruby, por Marcos Brizeno

An Introduction to Mocking in Python por Toptal

Python Mocking, You Are A Tricksy Beast tem explicações mais aprofundadas sobre como funcionam os mocks em Python, por Matt Pease


comments powered by Disqus