Integrating Spock with Maven/Eclipse

Introduction

In this post we will see a brief introduction about the Spock test framework and how to integrate it with Maven and Eclipse.

Spock

Spock is a framework used to write tests using the Groovy language and its adoption has been increasing lately. Although Groovy is the language you must use to write your tests with Spcok, it can be used for both Groovy and Java applications and is compatible with most of the Java IDEs, including Eclipse.
The idea of writing the tests using Groovy can be seen as an obstacle for some Java developers that are not familiar with Groovy. However, the knowledge required in Groovy is quite basic and the Spock documentation itself is very helpful with this question. I myself, right now, know very little about Groovy and this hasn’t been an issue to write tests with Spock.

Example

With the goal of showing how to use Spock along with Maven, let’s create a basic Java application, without any other framework. This application is basically comprised of a Customer entity, a service class CustomerServices that allows Customers to be added and queried in the system and, finally, an exception class InvalidCustomerException used when a Customer to be added is not valid. Let’s start by defining the structure of the pom.xml, as showed below:

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>

In the “maven-compiler-plugin” section, besides configuring the Java version as usual, we also set the value of the tag “compilerId” with “groovy-eclipse-compiler”. This configuration, along with the dependencies “groovy-eclipse-compiler” and “groovy-eclipse-batch”, are required to tell maven how to compile the Groovy classes. After that, we also declare the “maven-eclipse-plugin” plugin. This definition is done so that when we use the “mvn eclipse:eclipse” command to transform the project into an Eclipse project, it also comes with the characteristics of a Groovy project.
After the plugins definition, we are just importing the libraries JUnit, Spock and Groovy.

Having done that, we can proceed with the Java code. The first class to be created is Customer, which is just a simple POJO with 2 attributes.

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

public class Customer {

	private Long id;

	private String name;

	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 + "]";
	}

}

In order to map the possible validation errors, we will create the InvalidCustomerException exception.

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

/**
 * Exception 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;
	}

}

As we can see in the InvalidCustomerCause enum, a Customer to be considered valid needs to have both “id” and “name” fields filled and, besides that, the value of the field “id” can not have been already used for no other Customer. To finalize our application’s code, let’s add the CustomerServices class, responsible for holding (in memory) all the Customers added in the system and to make sure that only valid Customers can be added.

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;

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

}

Now our application is complete and we can create a test class using Spock to make sure that everything is working as expected. In order to do that, let’s create the class TestCustomerServices.groovy.

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

A few general explanations:

  • Every test class that makes use of Spock must extend from spock.lang.Specification
  • The setup() method is invoked before the execution of each one of the tests defined in the class.
  • The methods that contain test code are the ones with the name defined between quotes (“”).

Let’s now go through each one of the test cases:

“adds a new customer and finds him”

This String is the test case name and that’s the name you’ll see when execute it via Maven or Eclipse. Spock provides the “given/when/then” directives to support tests development and allows that each one of these directives receive a String explaining its goal. When some of them has more than one instruction, you can write them one bellow the other, or split them with the “and” directive, just like we did in this test case.
The code within the block “then” and its “and’s” is responsible to perform the assertions and check the expected result.

“adds a new customer with invalid data”

This test case introduces 2 interesting concepts: parameterized tests and errors/exceptions checking. In order to parameterize this test, we define in the method signature which are the parameterized parameters and through the “where” directive, we tell Spock that it must execute this method 3 times, being one for each line of the table with the corresponding values.
In order to check the exceptions to be thrown, we use the method “thrown” in the “then” clause. This tells Spock that, if no exception of the expected type is thrown, the test must fail. Assigning the exception to a variable is optional and only makes scence if we need to do something with it, like in the shown example.

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

It’s exactly equal to the test case above, the only difference is in the way of using the “where” directive to define the test execution parameters. In this case, instead of creating a table, we assign a variable for each one of the parameters along with their respective values.

In order to run this test cass, we can use either Maven or Eclipse.

Running the tests via Maven

Just run the command “mvn clean install” or “mvn clean package” and the test classes will be executed normally.

Running the tests via Eclipse

Just click with the right button over the name of the test class and then click in “Run as” -> “JUnit Test”. It’s important to note that in order to make Eclipse to recognize the groovy test class and to provide resources as auto-complete, it’s necessary to install the Groovy plugin “Groovy/Grails Tool Suite (GGTS)”, according to your Eclipse version. This plugin can be easily installed through Eclipse Marketplace.

Conclusion

This article’s goal was to introduce Spock framework and how to integrate it with Maven and Eclipse. Spock‘s website provides a fairly complete documentation with all of the framework’s features explained with details and examples. You can check the source code of this post here.

One thought on “Integrating Spock with Maven/Eclipse

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s