Os testes e os dublês - Parte 2

Continuando o artigo anterior, agora falarei sobre consultas de uma API externa


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 democking 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.

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 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 utilizandoQUnit 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, sobreRefactoring, 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

Via klauslaube.com.br

Total de comentários: 0
avatar