• Blog
    • Tecnologia
    • Carreira e Cultura
  • Materiais Gratuitos
  • Podcast
  • Nossas vagas
  • Site Movile
Menu
  • Blog
    • Tecnologia
    • Carreira e Cultura
  • Materiais Gratuitos
  • Podcast
  • Nossas vagas
  • Site Movile
  • Backend, Movile, Tecnologia, testes

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());
}

view raw
basic-test-structure.java
hosted with ❤ by GitHub

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
}
}

view raw
spock-organization.groovy
hosted with ❤ by GitHub

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

view raw
spock-validation.groovy
hosted with ❤ by GitHub

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

view raw
spock-and-block.groovy
hosted with ❤ by GitHub

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
}

view raw
data-driven-testing-1.groovy
hosted with ❤ by GitHub

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

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

view raw
max-2-number-output.sh
hosted with ❤ by GitHub

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
}

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

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

view raw
max-2-numbers-unroll-output.sh
hosted with ❤ by GitHub

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)

view raw
mock-creation-1.groovy
hosted with ❤ by GitHub

Ou utilizando a sintaxe Java:

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

view raw
mock-creation-2.groovy
hosted with ❤ by GitHub

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

  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

view raw
mock-interactions-constrain.bash
hosted with ❤ by GitHub

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"
}
}

view raw
stubbing-spock.groovy
hosted with ❤ by GitHub

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

view raw
stubbing-interaction.bash
hosted with ❤ by GitHub

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

view raw
exception-verification.groovy
hosted with ❤ by GitHub

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

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

view raw
exception-stubbing.groovy
hosted with ❤ by GitHub

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)

Compartilhe isso:

  • Tweet

Curtir isso:

Curtir Carregando...
Mario Rezende

Mario Rezende

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

Deixe um comentário

Categorias

Categorias
  • Android
  • Backend
  • Banco de Dados
  • BI
  • Carreira
  • Carreira e Cultura
  • Carreira e Cultura
  • Ciência de Dados
  • Cultura
  • Data Specialist
  • Design
  • Diversidade
  • Front-end
  • Frontend
  • Fundação 1Bi
  • Grupo Movile
  • Histórias
  • iFood
  • Infraestrutura
  • Inteligência Artificial
  • iOS
  • iOS App Development
  • Kotlin
  • kubernetes
  • LeaderShift
  • Material
  • Mobile
  • Mobile Dream
  • Movile
  • Movilian@s
  • News
  • PlayKids
  • Podcast
  • Produto
  • Projetos
  • React
  • RESPECT
  • Software Architecture
  • Software Engineering
  • Solid
  • Swift
  • SwiftUI
  • Sympla
  • Technology
  • Tecnologia
  • testes
  • UX
  • Vagas
  • Wavy
  • Zoop

Posts relacionados

Dart Extension Methods na prática

Leia mais »
Kubernetes

Papo Sobremesa #11 – Chapter Backend

Leia mais »
Jetpack

Utilizando a nova versão do Jetpack Paging

Leia mais »
SOLID

Swift: Princípio de Substituição de Liskov [Artigo 3]

Leia mais »
Tags
Agile Android Apache api App Apps Arquitetura Autoconhecimento Backend Banco de Dados BI Blog bot Bots Cache Carreira Carreira e Cultura Cloud code containers Continuos integration Cultura Dados Dados Probabilísticos data Data Center Data Science Desenvolvimento Design devs digital diversidade DSL Entrevista Evento eventos Experiências Facebook front Front-end Frontend Full-stack Fundação 1Bi Gestão GO google Groovy grupo Grupo Movile histórias home iFood Infraestrutura Inteligencia artificial iOS Java jetpack Json Kotlin kubernetes layout Liderança linguagem loadview Machine Learning marketplace Mobile Movile Movilianos news Objective-C PlayKids podcast produto Projetos pwa python Rapiddo react Reativas Redis research review RH Room spark Spring stack storyboards Superplayer Swift Sympla Talentos tdd Tecnologia Testes transformação digital Unity ux vagas Valores view viewcode viewcontroller viper Voxel vue wavy web Widget Zoop

Siga-nos

Facebook-f
Linkedin
Instagram
Youtube
Twitter

Baixe nosso e-book!

Receba conteúdos exclusivos em seu email!

Seus dados estão protegidos conosco.

Menu

  • Tecnologia
  • Carreira e Cultura
  • Materiais Gratuitos
  • Podcast
Menu
  • Tecnologia
  • Carreira e Cultura
  • Materiais Gratuitos
  • Podcast

principais categorias

  • Tecnologia
  • Carreira e Cultura
  • News
  • Movile
  • Backend
  • Android
Menu
  • Tecnologia
  • Carreira e Cultura
  • News
  • Movile
  • Backend
  • Android

FEED RSS

RSS Feed RSS - Posts

RSS Feed RSS - Comentários

redes sociais

  • Facebook
  • LinkedIn
  • Instagram
  • Youtube
  • Twitter

Copyright 2021 © Todos os direitos reservados. Criação de Site por UpSites & Weblab

  • Novas Vagas!

  • Último Podcast!

%d blogueiros gostam disto: