Integrando Spock com Maven/Eclipse

Introdução

Neste post vamos ver uma breve introdução sobre o framework de testes Spock e como inegrá-lo ao Maven a ao Eclipse.

Spock

Spock é um framework para construção e especificação de testes usando-se a linguagem Groovy e cuja adoção vem crescendo bastante ultimamente. Embora Groovy seja a linguagem com a qual você deve escrever seus testes usando Spock, ele pode ser usado tanto em aplicações Groovy quanto Java e é compatível com a maioria das IDEs Java, como Eclipse.
O fato de ser necessário escrever os testes em Groovy pode criar um pouco de aversão para quem programa em Java, mas não tem familiaridade com Groovy. Porém, o conhecimento que é preciso ter de Groovy é bem básico e a própria documentação do Spock ajuda com essa questão. Eu mesmo, no momento, sei muito pouco de Groovy e tenho usado Spock sem problemas.

Exemplo

Com o objetivo de mostrar o Spock na prática em conjunto com o Maven, vamos criar uma aplicação Java bastante simples, sem o uso de nenhum outro framework. Essa aplicação consiste basicamente de uma entidade Customer, uma classe de serviço CustomerServices que permite que Customers sejam adicionados e consultados e, por fim, uma classe de exceção InvalidCustomerException usada quando um Customer a ser inserido não é válido.
Para começar, vamos definir a estrutura do nosso pom.xml, conforme listagem abaixo:

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.wordpress.lucianomolinari</groupId>
	<artifactId>spockbasics</artifactId>
	<version>1.0.0</version>

	<name>Spock with Maven</name>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.1</version>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
					<compilerId>groovy-eclipse-compiler</compilerId>
				</configuration>
				<dependencies>
					<dependency>
						<groupId>org.codehaus.groovy</groupId>
						<artifactId>groovy-eclipse-compiler</artifactId>
						<version>2.8.0-01</version>
					</dependency>
					<dependency>
						<groupId>org.codehaus.groovy</groupId>
						<artifactId>groovy-eclipse-batch</artifactId>
						<version>2.1.8-01</version>
					</dependency>
				</dependencies>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-eclipse-plugin</artifactId>
				<version>2.9</version>
				<configuration>
					<additionalProjectnatures>
						<projectnature>org.eclipse.jdt.groovy.core.groovyNature</projectnature>
					</additionalProjectnatures>
					<sourceIncludes>
						<sourceInclude>**/*.groovy</sourceInclude>
					</sourceIncludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.11</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.spockframework</groupId>
			<artifactId>spock-core</artifactId>
			<version>0.7-groovy-2.0</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.codehaus.groovy</groupId>
			<artifactId>groovy-all</artifactId>
			<version>2.1.8</version>
			<scope>test</scope>
		</dependency>
	</dependencies>

</project>

No plugin “maven-compiler-plugin”, além de configurarmos a versão do Java como de costume, também preenchemos a tag “compilerId” com o valor “groovy-eclipse-compiler”. Essa configuração, juntamente com a definição das dependências “groovy-eclipse-compiler” e “groovy-eclipse-batch” são necessárias para instruir o maven a como compilar as classes escritas em Groovy. Na sequência, também definimos o plugin “maven-eclipse-plugin”. Essa definição é feita para que, quando usamos o comando “mvn eclipse:eclipse” para importar o projeto no Eclipse, esse projeto também venha com a característica de um projeto Groovy.
Após a definição dos plugins, simplesmente importamos a biblioteas JUnit, Spock e Groovy.

Com isso, podemos dar sequência com a criação do código Java. A primeira classe a ser criada é a Customer, que nada mais é do que um simples Pojo com 2 atributos.

Customer.java
package com.wordpress.lucianomolinari.spockbasics;

/**
 * Simple entity that represents a customer.
 * 
 * @author Luciano Molinari
 */
public class Customer {

	/**
	 * Identifies uniquely a {@link Customer}.
	 */
	private Long id;

	/**
	 * Name of the {@link Customer}.
	 */
	private String name;

	/**
	 * Creates a new {@link Customer} with the given id and name.
	 * 
	 * @param id
	 *            The identification of the customer.
	 * @param name
	 *            The name of the customer.
	 */
	public Customer(Long id, String name) {
		this.id = id;
		this.name = name;
	}

	public Long getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Customer other = (Customer) obj;
		if (id == null) {
			if (other.id != null)
				return false;
		} else if (!id.equals(other.id))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "Customer [id=" + id + ", name=" + name + "]";
	}

}

Para mapear os erros possíveis de validação, iremos criar uma classe de exceção chamada InvalidCustomerException.

InvalidCustomerException.java
package com.wordpress.lucianomolinari.spockbasics;

/**
 * Excpetion that must be thrown when a {@link Customer} to be persisted is not
 * valid.
 * 
 * @author Luciano Molinari
 */
public class InvalidCustomerException extends Exception {

	private static final long serialVersionUID = -6532930714866940079L;

	/**
	 * Declares the possible reasons of why a {@link Customer} is not valid.
	 */
	public enum InvalidCustomerCause {
		ID_NOT_INFORMED, NAME_NOT_INFORMED, DUPLICATED_ID
	}

	private InvalidCustomerCause invalidCustomerCause;

	/**
	 * Creates a new {@link InvalidCustomerException} with the given cause.
	 * 
	 * @param invalidCustomerCause
	 *            The reason why the {@link Customer} is invalid.
	 */
	public InvalidCustomerException(InvalidCustomerCause invalidCustomerCause) {
		this.invalidCustomerCause = invalidCustomerCause;
	}

	public InvalidCustomerCause getInvalidCustomerCause() {
		return invalidCustomerCause;
	}

}

Conforme podemos observar no enum InvalidCustomerCause, para que um Customer possa ser inserido, ele deve possuir os campos “id” e “name” preenchidos e, além disso, o valor do campo “id” não pode ter sido usado por nenhum outro Customer do sistema. Para finalizar o código da nossa aplicação, vamos criar a classe CustomerServices, responsável por manter os Customers adicionados em memória e garantir que somente Customers válidos sejam adicionados.

CustomerServices.java
package com.wordpress.lucianomolinari.spockbasics;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.wordpress.lucianomolinari.spockbasics.InvalidCustomerException.InvalidCustomerCause;

/**
 * Responsible for managing customers.
 * 
 * @author Luciano Molinari
 */
public class CustomerServices {

	/**
	 * Simple map to keep customers in memory.
	 */
	private static Map<Long, Customer> customers = new LinkedHashMap<Long, Customer>();

	/**
	 * Initializes the map {@link #customers}.
	 */
	public void init() {
		customers = new LinkedHashMap<>();
	}

	/**
	 * Persists a new {@link Customer} in the system.
	 * 
	 * @param customer
	 *            The {@link Customer} to be persisted.
	 * @throws InvalidCustomerException
	 *             If the {@link Customer} is not valid to be persisted.
	 */
	public void add(Customer customer) throws InvalidCustomerException {
		if (customer.getId() == null) {
			throw new InvalidCustomerException(InvalidCustomerCause.ID_NOT_INFORMED);
		}
		if (customer.getName() == null) {
			throw new InvalidCustomerException(InvalidCustomerCause.NAME_NOT_INFORMED);
		}
		if (customers.containsKey(customer.getId())) {
			throw new InvalidCustomerException(InvalidCustomerCause.DUPLICATED_ID);
		}
		customers.put(customer.getId(), customer);
	}

	/**
	 * @return A {@link List} with all the {@link Customer} of the system.
	 */
	public List<Customer> findAll() {
		return Collections.unmodifiableList(new ArrayList<>(customers.values()));
	}

}

Com isso, nossa aplicação está completa e podemos criar uma classe de testes usando Spock para garantir que tudo está funcionando conforme esperado. Para isso, criaremos a classe com.wordpress.lucianomolinari.spockbasics.TestCustomerServices.groovy dentro de src/test/java, como qualquer outra classe de testes.

TestCustomerServices.groovy
package com.wordpress.lucianomolinari.spockbasics

import com.wordpress.lucianomolinari.spockbasics.InvalidCustomerException.InvalidCustomerCause;

import spock.lang.Specification;

class TestCustomerServices extends Specification {

	private CustomerServices customerServices
	
	def setup() {
		customerServices = new CustomerServices()
		customerServices.init()
	}
	
	def "adds a new customer and finds him"() {
		given: "There is no customer in the system"
		customerServices.findAll().size() == 0
		
		and: "A customer called 'Joe' and identified by id 1"
		Customer joe = new Customer(1, "Joe")
		
		when: "Joe is inserted in the system"
		customerServices.add(joe)
		
		then: "Only one customer should exist in the system"
		List<Customer> customers = customerServices.findAll()
		customers.size() == 1
		
		and: "This customer should be Joe"
		customers.get(0).id == 1
		customers.get(0).name == "Joe"
	}
	
	def "adds a new customer with invalid data"(Long id, String name, InvalidCustomerCause expectedCause) {
		setup: "There is already a customer called John with id 1 in the system"
		customerServices.add(new Customer(1, "John"))
		
		when: "There is a request to insert a customer with the given parameters"
		customerServices.add(new Customer(id, name))
		
		then: "An error of type InvalidCustomerException should be thrown"
		InvalidCustomerException error = thrown(InvalidCustomerException.class)
		error.invalidCustomerCause == expectedCause
		
		where:
		id		| name			| expectedCause
		null	| "John"		| InvalidCustomerCause.ID_NOT_INFORMED
		1		| null			| InvalidCustomerCause.NAME_NOT_INFORMED
		1		| "Carl"		| InvalidCustomerCause.DUPLICATED_ID
	}
	
	def "adds a new customer with invalid data - version 2"(Long id, String name, InvalidCustomerCause expectedCause) {
		setup: "There is already a customer called John with id 1 in the system"
		customerServices.add(new Customer(1, "John"))
		
		when: "There is a request to insert a customer with the given parameters"
		customerServices.add(new Customer(id, name))
		
		then: "An error of type InvalidCustomerException should be thrown"
		InvalidCustomerException error = thrown(InvalidCustomerException.class)
		error.invalidCustomerCause == expectedCause
		
		where:
		id << [null, 1, 1]
		name << ["John", null, "Carl"]
		expectedCause << [InvalidCustomerCause.ID_NOT_INFORMED, InvalidCustomerCause.NAME_NOT_INFORMED, 
			InvalidCustomerCause.DUPLICATED_ID]
	}
	
}

Algumas explicações gerais:

  • Toda classe de testes que use o Spock deve extender spock.lang.Specification
  • O método setup() é chamado antes da execução de cada um dos testes definidos na classe.
  • Os métodos com código de teste são os que tem seu nome definido entre “”.

Vamos agora analizar cada um dos testes:

“adds a new customer and finds him”

Essa string é o nome do caso de teste e é esse nome que você verá quando executá-lo via Maven ou Eclipse. O Spock provê as diretivas “given/when/then” para a construção de testes e permite que cada diretiva dessa receba uma String explicando o objetivo daquele bloco. Quando alguma dessas diretivas possue mais de 1 instrução, você pode escrevê-las uma embaixo da outra, ou separá-las com a diretiva “and”, conforme fizemos nesse caso de teste.
O código que estiver no bloco “then” e nos seus “and”s é responsável por fazer as asserções do teste e verificar o resultado esperado.

“adds a new customer with invalid data”

Esse caso de teste traz 2 conceitos interessantes: testes parametrizados e verificação de erros. Para a parametrização desse teste, definimos na assinatura do método quais são os valores parametrizáveis e através da diretiva “where”, informamos ao Spock que ele deve executar esse método 3 vezes, uma para cada linha da tabela com os respectivos valores.
Para verificação de exceções que devem ser lançadas, usamos o método thrown na cláusula then. Isso informa ao Spock que, caso uma exceção do tipo informado não seja lançada, o teste deve falhar. A atribuição da exceção à uma variável é opcional e só faz sentido em casos que precisamos fazer algo com a variável, como no caso mostrado.

“adds a new customer with invalid data – version 2”

Exatamente igual ao caso de teste acima, a única diferença é na forma de utilização da diretiva “where” para definição dos parâmetros de execuções do teste. Nesse caso, ao invés de criarmos uma tabela, definimos uma variável para cada um dos parâmetros e seus possíveis valores.

Para executarmos a classe de teste, podemos usar o Maven ou o Eclipse.

Executando os testes via Maven

Basta executar o comando “mvn clean install” ou “mvn clean package” que todas as classes de teste serão executadas normalmente.

Executando os testes via Eclipse

Basta clicar com o botão direito sobre o nome da classe de teste, “Run as” -> “JUnit Test”. Importante notar para que o Eclipse reconheça a classe de teste groovy e forneça recursos como auto-complete, é necessário instalar o Plugin para o Groovy “Groovy/Grails Tool Suite (GGTS)”, de acordo com a sua versão do Eclipse. Esse plugin pode ser instalado facilmente através do Eclipse Marketplace.

Conclusão

O objetivo desse artigo foi de apresentar o framework Spock e como integrá-lo ao Maven e ao Eclipse. O site do Spock fornece uma documentação bem completa com todas as features do framework explicadas em detalhes e com exemplos. O código fonte do projeto pode ser encontrado aqui.


Quer aprender muito mais? Não deixe de ver meu curso Construa uma aplicação do zero com JEE 7, Java 8 e Wildfly.

Anúncios
Esta entrada foi publicada em Tests com as etiquetas , , , . ligação permanente.

Deixe uma Resposta

Preencha os seus detalhes abaixo ou clique num ícone para iniciar sessão:

Logótipo da WordPress.com

Está a comentar usando a sua conta WordPress.com Terminar Sessão / Alterar )

Imagem do Twitter

Está a comentar usando a sua conta Twitter Terminar Sessão / Alterar )

Facebook photo

Está a comentar usando a sua conta Facebook Terminar Sessão / Alterar )

Google+ photo

Está a comentar usando a sua conta Google+ Terminar Sessão / Alterar )

Connecting to %s