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.