Quem já escreve testes há algum tempo deve ter percebido que, para cada funcionalidade da aplicação que queremos testar, temos que escrever ao menos um cenário que cobre o “caminho feliz” e um ou mais cenários que cobrem os “caminhos infelizes”. Escrevemos muito mais código na suíte de testes em comparação ao código que será executado de fato em produção.
Tendo isso em vista, a ferramenta que utilizamos para dar suporte a criação de testes têm impacto significativo na velocidade de desenvolvimento, manutenção e evolução da aplicação.
Caminho feliz: cenário onde o cliente utiliza a aplicação de acordo com as especificações do projeto.
Caminho infeliz: são as possibilidades de utilização incorretas da aplicação pelo cliente (exceções).
Neste artigo vou apresentar o Spock, um framework de testes para aplicações Java e Groovy, e como sua linguagem de especificação elegante e expressiva pode trazer maior produtividade no dia-a-dia do programador.
Analisaremos o Spock através das seguintes características:
- Legibilidade e organização do teste
- Versatilidade de parametrização do teste
- Reportando falhas
- Testando a interação entre objetos: “Mocking” e “Stubbing”
- Trabalhando com exceções
Legibilidade e organização do teste
Praticamente todo teste que escrevemos (salvo algumas exceções) seguem as seguintes fases:
- Configuração: onde inicializamos os dados que utilizaremos no teste.
- Execução: onde o objeto que está sendo testado será executado.
- Verificação: onde avaliamos os resultados retornados na fase de execução.
Vamos exemplificar essas fases escrevendo um cenário que testará uma funcionalidade fictícia de cadastro de usuário com JUnit 5:
Embora essas fases estejam presentes na maioria dos testes, nem todas as bibliotecas dispoẽm de mecanismos para deixar isso de forma explícita; no caso do JUnit costumo usar comentários para demarcar cada fase.
Reescrevendo esse mesmo cenário com o Spock o teste fica bem mais legível e organizado, graças a linguagem Groovy (base do Spock) que nos permite escrever os métodos de forma declarativa e manter o corpo do teste mais simples e conciso. O Spock impõe a separação das fases ao estilo BDD, utilizando os blocos: “given”, “when” e “then”.
A fase de verificação também fica mais simples, pois utiliza os mesmos operadores de comparação do Java (removendo a necessidade de conhecer os métodos de “assertion”):
Dica: existe também o bloco “and” que pode ser usado para deixar a especificação do teste mais compreensível:
Versatilidade de parametrização do teste
Com frequência precisamos executar o teste com dados diferentes para garantir que todos os cenários possíveis estão sendo validados.
O Spock resolve esse problema através do bloco “where” onde podemos prover uma lista de dados a serem utilizados em nossos testes.
No exemplo abaixo utilizamos o “Data Tables” para fornecer os dados de entrada ao método do teste. A primeira linha da tabela (header) declara o nome das variáveis que utilizaremos no teste, e as demais linhas correspondem ao valor dessas variáveis. Para cada linha o método do teste será executado uma vez.
Organizando os dados no formato de tabela facilita tanto para quem escreve, como também para quem lê o teste; fazendo com que o teste sirva como uma documentação do funcionamento do objeto sob teste.
O Spock utiliza o conceito de “Data Driven Testing”, que fornece diversas formas de gerarmos dados de entrada, dando mais flexibilidade ao programador durante a escrita dos testes:
- Data Tables
- Data Pipes
- Multi-Variable Data Pipes
- Data Variable Assignment
- Combinação dos itens acima
Reportando falhas
No teste abaixo inserimos uma falha propositalmente na tabela do bloco “where” (linha 9):
Dica: prefira utilizar o bloco “expect” quando a execução e a validação podem ser descritas numa única fase.
Ao executar o teste teremos a seguinte saída no terminal:
Com Spock é fácil de identificar que a falha ocorreu na segunda interação, mas podemos deixar essa informação ainda mais explícita utilizando a anotação de “@Unroll”:
Perceba que inserimos marcações no nome do método usando o sinal “#” mais o nome das variáveis utilizadas no teste. Executando o teste novamente teremos a seguinte saída:
Identificar rapidamente onde ocorreu a falha aumenta a nossa produtividade, principalmente numa suíte de teste muito grande.
Testando a interação entre objetos
Mocking
Nem só de verificação de estado vivem nossos testes; há cenários onde é necessário explorar o comportamento do objeto sob teste do ponto de vista das interações que ele faz com outros objetos.
Nesse tipo de teste (também conhecido como “Interaction Tests” ou “Collaboration Test”) utilizamos a técnica de “Mock” (imitar) para conseguirmos analisar as interações entre os objetos sob teste.
A criação de “Mocks” é muito simples e pode ser feita de duas formas:
Ou utilizando a sintaxe Java:
Vejamos o uso do “Mock” mais detalhadamente no exemplo abaixo (linhas 14, 15):
Diferente dos exemplos anteriores, nos quais verificamos o estado do objeto sob teste (ex: user.getName() == name
); nosso interesse agora é saber se após a fase execução (“when”) o objeto “Publisher” realizou as interações necessárias com os seus colaboradores “Subscriber”.
A análise das interações é feita através de restrições, ou seja, no exemplo acima esperamos que objeto alvo “Subscriber”, seja chamado através do método “receive”, recebendo exatamente como argumento a palavra “hello” e que isso ocorra apenas um vez.
Essas restrições estão organizadas no Spock da seguinte forma:
- restrição por cardinalidade
- restrição de alvo
- restrição de método
- restrição de argumento
Obs: todas as variações de restrição podem ser vistas na documentação do Spock.
Stubbing
Como podemos testar um objeto, de forma independente, quando ele depende do resultado retornado por outros objetos que colaboram com ele?
É preciso controlar os objetos de colaboração e definir como eles devem se comportar durante as interações com o objeto sob teste.
Chamamos essa técnica de “Stub”, onde substituímos o objeto real por um objeto que será alimentado com as entradas que desejamos utilizar no nosso testes.
Alteramos o nosso exemplo de teste que agora utiliza o “Stubbing” para definir um resultado ao método “receive” da interface “Subscriber” (linha 22). Dessa forma podemos validar não apenas a interação mas também o retorno esperado:
As interações com “Stubs” diferem um pouco em comparação as interações como “Mocks”, conforme demonstrado abaixo:
Mais uma vez, todas as variações de interações pode ser vistas na documentação do Spock, especialmente a parte que trata da combinação de “Mocking” e “Stubbing”.
Trabalhando com exceções
Por fim, porém não menos importante, precisamos ser capazes de validar quando o objeto sob teste deve lançar uma exceção.
Geralmente, o uso de exceções nos testes ocorrem de duas formas:
1 - Validar a ocorrência de uma exceção (“Exception Conditions”).
2 - Simular uma exceção como efeito colateral (utilizando “Stubs”).
Conclusão
Ainda como pontos positivos do Spock vale listar:
- Spock disponibiliza um “Web Console” onde podemos experimentar o seu funcionamento.
- Projeto de exemplo mostrando como configurar com: Ant, Gradle e Maven.
- Ótima documentação, embasada em conceitos do Agile e BDD.
- O framework de “Mock” do Spock é integrado, ou seja, não é preciso importar outras bibliotecas (embora também seja possível utilizá-las em conjunto com o Spock).
- Integração com o Spring.
Nem tudo são flores. Importante ressaltar que, em comparação, o JUnit 5 ganha nos seguintes pontos:
- 100% implementado em Java.
- Melhor compatibilidade com as últimas versões da JDK.
- Melhor integração com as IDEs.
Enfim, desde que utilizei o Spock pela primeira vez não larguei mais; não apenas pelo ganho de produtividade, mas também pela forma como os testes passam a funcionar (de fato) como a documentação da aplicação.
Referências:
Top comments (0)