quinta-feira, 14 de março de 2013

Testes com Mockito e um pouquinho de ScalaTest

Recentemente, retornando ao TDD com Java, resolvi experimentar o Mockito. Descobri um mocking framework muito interessante, fácil de utilizar e versátil.


A documentação é relativamente ampla, por isso só vou dar um breve relato de como foi minha experiência, com o intuito também de motivá-lo a explorar mais essa biblioteca e testar em seus projetos.





Antes de tudo, é pertinente nos perguntarmos: Para quê servem mocks, doubles, stubs?
Antes de responder, há uma ideia de que double é um termo mais genérico que abrange tanto mock quanto stub.

Resposta para a pergunta:
  • Nem sempre interessa uma instância real de algum objeto, com todas as suas funcionalidade e atributos. Muitas vezes você só vai precisar do objeto por seu tipo (classe que instancia). Nesses casos, criar uma instância real é um desperdício de processamento (e possivelmente de implementação) em seus testes.
  • Mais alterações podem acontecer nas classes sem afetar os testes. Por exemplo, alterações de construtor, como veremos em exemplos mais adiante.
  • Pode ser interessante, para fins de testes, verificar se um determinado método foi chamado, quantas vezes foi chamado, ou interceptá-lo para que retorne um valor padrão, por exemplo, em métodos que fazem acesso a outras camadas que envolvem processamento maior ou até mesmo um acesso remoto irrelevante para o teste atual.

Provavelmente essa lista poderia ainda aumentar bastante, mas acredito que já deu pra dar uma ideia de situações bem comuns em que doubles são bastante úteis.



Exemplo - mock()


Suponha que você tem uma classe cujo construtor exige a passagem de três parâmetros. Por algum motivo, você precisa de uma lista de elementos dessa classe, mas utilizar o caminho normal para instanciar os objetos exigiria um código que não é pertinente ao teste.


List<Item> itens = controle.getItens();

itens.add(new Item(arg1, arg2, arg3));
itens.add(new Item(arg1, arg2, arg3));
itens.add(new Item(arg1, arg2, arg3));
itens.add(new Item(arg1, arg2, arg3));
itens.add(new Item(arg1, arg2, arg3));

assertEquals(5, itens.size());

No código acima, perceba que o que é realmente pertinente ao teste é o tamanho da lista populada com cinco instâncias da classe Item. Em primeiro lugar, a forma como cada objeto Item foi instanciado é irrelevante. Em segundo lugar, qualquer alteração na assinatura do método construtor implicará um erro de compilação nos testes, que necessitarão de ajuste.

Resumindo, seria ótimo poder obter as instâncias de Item sem se preocupar com esses detalhes e concentrando mais na nossa verificação final do tamanho da lista. O exemplo abaixo resolve o problema:

import static org.mockito.Mockito.mock;

...
...

List<Item> itens = controle.getItens();

itens.add(mock(Item.class);
itens.add(mock(Item.class);
itens.add(mock(Item.class);
itens.add(mock(Item.class);
itens.add(mock(Item.class);

assertEquals(5, itens.size());


Exemplos - Stub


O recurso de stub adiciona a um mock um comportamento ou retorno padrão para algum método.
Por exemplo, para meus testes, nada me importa sobre a classe Item, exceto que preciso de um retorno válido ao ler o atributo de descrição.

import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

...

Item item = mock(Item.class);

when(item.getDescricao()).thenReturn("Item 1");
when(item.setDescricao(anyString()).thenThrow(AutorizacaoException.class);

item.getDescricao(); // Retorna "Item 1"
item.setDescricao("Item 2"); // Lança exceção
...

O exemplo acima traz alguns conceitos novos e bastante interessantes. O método when permite forçar um comportamento por parte do objeto mock. Na linha 9, é definido que o método getDescricao() deverá retornar "Item 1", o que é confirmado com uma chamada como a da linha 12. E na linha 10, define-se que o método setDescricao(), invocado com qualquer String por parâmetro, lançará uma exceção. É especialmente interessante o uso dos métodos estáticos any...() oferecidos pela classe org.mockito.Matchers.


Exemplo um pouco mais avançado



Mockito oferece muitos outros recursos que vale muito a pena conferir. A documentação é muito boa e os exemplos muito claros, por isso não há real necessidade de escrever mais por aqui. Mas, só pra atiçar um pouco mais a curiosidade, dou um outro exemplo abaixo com alguns outros métodos que me foram muito úteis.
E pra dar um gostinho especial, o código abaixo não está em Java, mas sim Scala que, para quem desconhece, é uma linguagem diferente de Java mas que roda em cima da JVM assim como Java, compila em bytecode e tudo mais. O código fica bem mais simples e todas as bibliotecas Java do seu projeto podem ser utilizadas em conjunto de forma natural. Para os testes, eu achei uma excelente combinação.

import org.mockito.Matchers.anyListOf
import org.mockito.Matchers.anyObject
import org.mockito.Matchers.anyString
import org.mockito.Mockito.doCallRealMethod
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify

... 

var mockReader = mock(classOf[Reader])
  
doCallRealMethod.when(mockReader).readFile(anyString)
doCallRealMethod.when(mockReader).readLines(anyListOf(classOf[String]))
  
var lines = mockReader.readFile(ReaderTest.TEST_FILE)

mockReader.readLines(lines)

verify(mockReader, times(lines.size)).buildRecord(anyString)
verify(mockReader, times(lines.size)).addRecord(anyObject())

...



Hmmm, agora realmente ficou interessante!! Sim, ficou mesmo.
Na linha 11 criei o mock
Nas linhas 13 e 14 defini que, apesar de meu objeto ser um mock, desejo que os métodos readFile() e readLine() tenham a sua real implementação invocada.
Na linha 16 leio um arquivo de texto guardando a lista de String resultante
Na linha 18 passo essa lista por parâmetro para o método readLines()
E nas linhas 20 e 21 verifico se a quantidade de itens na lista lines é a mesma quantidade de chamadas feitas aos métodos buildRecord (com qualquer String por parâmetro) e addRecord (com qualquer objeto por parâmetro).


Concluindo, Mockito é uma biblioteca muito interessante para ser utilizada em seus testes simplificando-os e ainda acrescentando algumas verificações que, somente com JUnit, não são possíveis. Há vários recursos que não abordei aqui, como spy, algumas annotations, etc. A linguagem Scala também é sensacional, muito simples, muito flexível e a sua biblioteca ScalaTest permite escrever testes mais rápido e com maior preocupação no teste propriamente dito do que na sintaxe da linguagem. Sobre isso, pretendo abordar em maior profundidade em um novo post.


Até mais!

Nenhum comentário:

Postar um comentário