Spock Framework: Teste Java com mais produtividade

Compartilhe

Compartilhar no facebook
Compartilhar no google
Compartilhar no twitter
Compartilhar no linkedin

Transforme a prática de escrever teste numa tarefa mais prazerosa

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”
    • “Stubbing”
  • Trabalhando com exceções

Legibilidade e organização do teste

Praticamente todo teste que escrevemos (salvo algumas exceções) seguem as seguintes fases:

  1. Configuração: onde inicializamos os dados que utilizaremos no teste.
  2. Execução: onde o objeto que está sendo testado será executado.
  3. 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:

@Test
void validateUserRegistration() {
// setup
String name = "Fulano"
int age = 20
String email = "fulano@mail.test"
// execute
User user = UserService().register(name, age, email)
// verify
assertEquals(name, user.getName());
assertEquals(age, user.getAge());
assertEquals(email, user.getEmail());
}

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

def 'Validate user registration'() {
given: 'user data'
def name = "Fulano"
def age = 20
def email = "fulano@mail.test"
when: 'register the user'
def user = UserService().register(name, age, email)
then:
with(user) {
getName() == name
getAge() == age
getEmail() == email
}
}

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”):

then:
name == 'Fulano'
name != 'Beltrano'
age >= 20
age < 100

Dica: existe também o bloco “and” que pode ser usado para deixar a especificação do teste mais compreensível:

given: 'open a database connection'
// code goes here
and: 'seed the customer table'
// code goes here
and: 'seed the product table'
// code goes here

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.

def 'Allow user access only for 18 over'(name, age, expected) {
given: 'user entity'
def user = new User(name, age)
when: 'validate user access'
def userCanAccess = AccessService().grantUser(user)
then:
userCanAccess == expected
where:
name | age | expected
'Fulano' | 18 | true
'Beltrano' | 19 | true
'Sicrano' | 17 | false
}

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:

Reportando falhas

No teste abaixo inserimos uma falha propositalmente na tabela do bloco “where” (linha 9): 

def 'Maximum of two numbers'() {
expect:
Math.max(a, b) == c
where:
a | b | c
1 | 3 | 3
7 | 4 | 4
0 | 0 | 0
}

view raw
max-2-numbers.groovy
hosted with ❤ by GitHub

Dica: prefira utilizar o bloco “expect” quando a execução e a validação podem ser descritas numa única fase.

Ao executar esse teste teremos a seguinte saída no terminal:

Condition not satisfied:
Math.max(a, b) == c
| | | | | |
| 7 7 4 | 4
| false
class java.lang.Math

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”:

@Unroll
def 'Maximum of #a and #b is #c'() {
expect:
Math.max(a, b) == c
where:
a | b | c
1 | 3 | 3
7 | 4 | 4
0 | 0 | 0
}

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: 

maximum of 1 and 3 is 3 PASSED
maximum of 7 and 4 is 4 FAILED
Math.max(a, b) == c
| | | | | |
| 7 7 4 | 4
| false
class java.lang.Math
maximum of 0 and 0 is 0 PASSED

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:

def subscriber1 = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)

Ou utilizando a sintaxe Java:

Subscriber subscriber1 = Mock()
Subscriber subscriber2 = Mock()

Vejamos o uso do “Mock” mais detalhadamente no exemplo abaixo (linhas 14, 15):

class Publisher {
List<Subscriber> subscribers = []
void send(String message){
subscribers*.receive(message)
}
}
interface Subscriber {
void receive(String message)
}
class PublisherSpec extends Specification {
Publisher publisher = new Publisher()
Subscriber subscriber1 = Mock()
Subscriber subscriber2 = Mock()
def setup() {
publisher.subscribers << subscriber1 // << operador do Groovy para List.add()
publisher.subscribers << subscriber2
}
def 'should send messages to all subscribers'() {
when:
publisher.send("hello")
then:
1 * subscriber1.receive("hello")
1 * subscriber2.receive("hello")
}
}

view raw
mock-examples.groovy
hosted with ❤ by GitHub

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 alvoSubscriber”, seja chamado através do métodoreceive”, 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:

  1. restrição por cardinalidade
  2. restrição de alvo
  3. restrição de método
  4. restrição de argumento
1 * subscriber1.receive("hello")
| | | |
| | | restrição de argumento
| | restrição de método
| restrição de alvo
restrição por cardinalidade

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: 

class Publisher {
private Subscriber subscriber
Publisher(Subscriber subscriber) {
this.subscriber = subscriber
}
String sendAndGetStatus(String message) {
return subscriber.receive(message)
}
}
interface Subscriber {
String receive(String message)
}
class PublisherSpec extends Specification {
Publisher publisher
Subscriber subscriber = Mock()
def setup() {
subscriber.receive(_) >> "ok" // Stubbing
publisher = new Publisher(subscriber)
}
def 'should send messages and get the subscriber status'() {
when:
def result = publisher.sendAndGetStatus("hello")
then:
result == "ok"
}
}

As interações com “Stubs” diferem um pouco em comparação as interações como “Mocks”, conforme demonstrado abaixo:

subscriber.receive(_) >> "ok"
| | | |
| | | gerador de resposta
| | argumento de restrição
| método de restrição
objeto de restrição

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”).

given:
def stack = new Stack()
when:
stack.pop()
then:
thrown(EmptyStackException)
stack.empty

2) Simular uma exceção como efeito colateral (utilizando “Stubs”).

subscriber.receive(_) >> { throw new InternalError("ouch") }

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:

  • Why I prefer Spock over JUnit by Bouke Nijhuis (YouTube)
  • Spock vs JUnit 5 – Clash of the Titans by Marcin Zajaczkowski (YouTube, Slides)
  • XUnit Test Patterns – Refactoring Test Code by Gerard Meszaros (Site, Book)
Mario Rezende

Mario Rezende

Backend Specialist no iFood. Entusiasta em Java, TDD, Hexagonal Architecture e CQRS.

Deixe um comentário

Categorias

Posts relacionados

Siga-nos

Baixe nosso e-book!

%d blogueiros gostam disto: