Os testes e os dublês - Parte 2

TDD (izenbridge.com)

No post anterior, vimos um dos cenários de testes utilizados por times da Globo.com, onde não escrevemos testes "isolados" (famigerados microtests), e abusamos da integração entre classes e serviços.

Mas até mesmo para nós existe um limite que não podemos ultrapassar: O caso de uma consulta a uma API externa, por exemplo. Nesse cenário, precisamos fingir que estamos fazendo isso, sem perder a segurança em nossas asserções.

Dublês ao resgate

Como já mencionado, os test doubles têm por finalidade substituir um objeto real, afim de validar algum conceito em nossos testes.

Para auxiliar-nos nessa demanda, vamos utilizar a Python Mock, biblioteca de mocking padrão do Python 3. Com ela, poderemos fingir integrações complexas da nossa aplicação, com o objetivo de testar um determinado comportamento em "custo" e tempos atrativos.

I find your lack of tests disturbing (jasonpolites.github.io)

Se você (assim como eu) ainda está no Python 2.7.X, podemos instalar a lib através do pip:

$ pip install mock

Mais instruções para instalação da Mock.

Uma vez instalada, podemos partir para conceituar de forma prática cada um dos tipos de dublês listados no post anterior.

Dummys

Um Dummy Object é um tipo de dublê muito simples, que é usado apenas para preencher passagens de parâmetros:

class Carro:
    rodas = 4

    def __init__(self, descricao, fabricante):
        self.descricao = descricao
        self.fabricante = fabricante

# Teste

def test_usando_dummy():
    fabricante = None
    carro = Carro('Fusca', fabricante)

    assert carro.rodas == 4

Como observado, um Dummy não precisa ser necessariamente um mock. Um valor em branco, nulo, uma string vazia, qualquer coisa usada para substituir um objeto real em uma passagem de parâmetro, pode ser considerado um Dummy.

Fakes

Um Fake é um objeto com certa funcionalidade, muito útil para resolver alguma dependência em testes, mas que não é ideal para o ambiente de produção:

class Carro:
    rodas = 4

    def __init__(self, descricao, fabricante):
        self.descricao = descricao
        self.fabricante = fabricante

    def __str__(self):
        return "{0} ({1})".format(
            self.descricao,
            self.fabricante.get_descricao(),
        )

# Teste

class FabricanteFake:
    descricao = 'Volkswagen'

    def get_descricao(self):
        return self.descricao

def test_usando_fake():
    fabricante = FabricanteFake()
    carro = Carro('Fusca', fabricante)

    assert str(carro) == 'Fusca (Volkswagen)'

No exemplo acima, (ainda) não utilizamos nenhum recurso da Mock. Dependendo do contexto, não precisamos de uma biblioteca para criarmos um Fake, e isso pode ser encarado de forma positiva, pois fica muito clara a nossa intenção no teste.

É relativamente comum vermos Fakes sendo utilizados para "dublar" acessos a um banco de dados. Quando falamos de testes em Django, geralmente utilizamos uma persistência mais leve (como um banco SQLite, por exemplo) que substitui um banco mais complexo, tornando a nossa suíte de testes mais simples.

Mocks

Um tipo de dublê criado para um teste específico. Com ele, somos capazes de setar retornos de valores pré-definidos, bem como verificar se algum método foi chamado durante a execução do teste:

class Carro:
    rodas = 4

    def __init__(self, descricao, fabricante):
        self.descricao = descricao
        self.fabricante = fabricante

    def __str__(self):
        return "{0} ({1})".format(
            self.descricao,
            self.fabricante.get_descricao(),
        )

# Teste

def test_usando_mock():
    fabricante = MagicMock()
    fabricante.get_descricao.return_value = 'Volkswagen'
    carro = Carro('Fusca', fabricante)

    assert str(carro) == 'Fusca (Volkswagen)'
    fabricante.get_descricao.assert_called_once_with()

Mocks são fundamentais quando estamos lidando com interações das quais não podemos (ou fica muito custoso) prever o comportamento. Particularmente, gosto de usar Mocks para garantir que o contrato entre o meu método/classe e meu serviço/API esteja coerente.

Stubs

Semelhantes aos Mocks, com os Stubs temos a capacidade de retornar respostas pré-definidas durante a execução de um teste. A principal diferença entre ambos é que com Stubs, não costumamos checar se eles foram propriamente executados:

class Carro:
    rodas = 4

    def __init__(self, descricao, fabricante):
        self.descricao = descricao
        self.fabricante = fabricante

    def __str__(self):
        return "{0} ({1})".format(
            self.descricao,
            self.fabricante.get_descricao(),
        )

# Teste

from mock import MagicMock


def test_usando_stub():
    fabricante = MagicMock()
    fabricante.get_descricao.return_value = 'Volkswagen'
    carro = Carro('Fusca', fabricante)

    assert str(carro) == 'Fusca (Volkswagen)'

test_usando_stub()

Stubs são excelentes para fingir interações com bibliotecas third-party. Não precisamos compreender a sua complexidade, apenas fingimos que ela está lá e retornando valores para os nossos test cases. Exemplo:

from datetime import date
with patch('mymodule.date') as mock_date:
    mock_date.today.return_value = date(2010, 10, 8)
    mock_date.side_effect = lambda *args, **kw: date(*args, **kw)

    assert mymodule.date.today() == date(2010, 10, 8)
    assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)

Spies

Com Spies, ao invés de setarmos expectativas, armazenamos as chamadas realizadas por colaboradores:

class Carro:
    rodas = 4

    def __init__(self, descricao, fabricante):
        self.descricao = descricao
        self.fabricante = fabricante

    def __str__(self):
        return "{0} ({1})".format(
            self.descricao,
            self.fabricante.get_descricao(),
        )

# Teste

from mock import MagicMock


def test_usando_spy():
    fabricante = MagicMock()
    fabricante.get_descricao.return_value = 'Volkswagen'

    fusca = Carro('Fusca', fabricante)
    gol = Carro('Gol', fabricante)

    str(fusca)
    str(gol)

    assert fabricante.get_descricao.call_count == 2

Costumo usar Spies com frequência em testes front-end, principalmente utilizando QUnit e Sinon.JS, para garantir a chamada de um determinado método dentro de eventos complexos, onde não consigo ter certeza sobre o resultado esperado.

Conclusão

Já dizia o filósofo que "mockar é uma arte". A verdade é que o uso de doubles nos ajuda muito quando estamos trabalhando dentro de um contexto de TDD, simplificando assim um relacionamento complexo entre classes/objetos, afim de agilizar o nosso desenvolvimento e facilitar os nosso testes.

Recentemente participei de um treinamento da Industrial Logic, sobre Refactoring, e a lição que ficou foi: Use mocks moderadamente. Sempre dê preferência a uma alteração na arquitetura do seu software (como por exemplo, o uso de Injeção de Dependência).

Se o uso de dublês for inevitável, prefira tipos mais simples (Dummys e Fakes). Dessa forma, os seus testes ficarão simples, legíveis e mais fáceis de manter.

Até a próxima.

Referências