How to Create Concise Data-Driven Unit Tests With the Spock Framework

February 28, 2023
Written by
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Twilion

header - How to Create Concise Data-Driven Unit Tests With the Spock Framework

Unit tests are significant in software engineering to create better and more reliable software services. It is common to come across software use cases that strongly rely on the input data, often called Data-Driven Tests.

Spock is a test framework created with the Groovy language, compatible with any Java project. Spock makes creating Data-Driven Tests fun and easy due to its simplicity and expressiveness.

In this post, you’ll learn how to write concise data-driven unit tests with the Spock Framework.

Prerequisites

For this tutorial, you'll need a couple of things before starting:

A brief introduction to Groovy and Spock

Groovy is an object-oriented language compiled and interpreted by the Java Virtual Machine (JVM). It is a language fully compatible with any Java project since Groovy classes are compiled just like Java classes.

The difference between Java and Groovy is mostly their syntax. Groovy uses dynamic typing, has more built-in functions, and has a more expressive language. These features make Groovy more flexible and more straightforward than pure Java.

Spock is a test framework written in Groovy that runs using the JUnit test runner architecture. That makes Spock compatible with most Java enterprise applications, IDEs, and infrastructure.

Groovy's simplicity and expressiveness make Spock a good candidate for a test framework. Spock produces more concise and clear code when compared to pure-java test frameworks like JUnit and Mockito.

I’ll show you in the following sections:

  • How to set up the Spock Framework in a Spring Application.
  • The definition and some examples of data-driven tests for a demo application.
  • The next steps with unit tests and the Spock Framework.

Set up the Application

Initialize the Spring Application

The first step is to create a template Spring application with the Spring initializr tool. To do so:

  1. Go to the website https://start.spring.io/.
  2. Choose Maven as the build system.
  3. Choose Java as the language.
  4. Pick a 2.7 version of the Spring Boot.
  5. Change the artifact name to spockdemo.
  6. Choose JAR as the packaging tool.
  7. Pick version 17 of Java.
  8. Click on Generate and save the project on your computer.

The final result of the initializer configuration is like the image below:

Initial configurations for a demo Spring application using the Spring Initializr tool

Configure the Application using IntelliJ IDEA

After clicking on Generate at the Spring Initializr, the website downloads a .zip file. To import the project into IntelliJ IDEA, do the following steps:

  1. Extract the downloaded .zip file using your favorite tool.
  2. Open IntelliJ IDEA. Once there, click on File->Open at the top left corner and choose the extracted archive.

Wait a few seconds for the IDE to index the imported files. After indexation, it is time to pull the Spock maven dependency into our project. To do so, add the following lines anywhere inside the <dependencies> tag at your pom.xml file located in the root directory of the project:

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <version>2.2-groovy-4.0</version>
    <scope>test</scope>
</dependency>

After adding this dependency, you’ll need to reload the project’s dependencies. To do so, go to the Maven menu in the top right corner of IntelliJ IDEA and click on the Reload All Maven Projects button, highlighted in the image below.

Maven goals with reload dependencies option highlighted

The final step is to create the test folder to store Spock tests. It is a good practice to keep them separated from the Java tests. To do that:

  1. Right-click on the test folder (which is within the src folder of your project directory) and choose the New->Directory option.
  2. Name the directory groovy.
  3. Right-click on the groovy directory and go to Mark Directory As->Test Sources Root.

Create the Test Target Java Class

In this tutorial, I’ll use a Body Mass Index (BMI) calculator to be the target application of unit tests. That calculator receives a person's height and weight and returns the BMI category. The equation and table below show how to calculate the BMI value and the interval for each BMI category.

Equation:
BMI value = [weight (kg) / height (cm) / height (cm)] x 10,000

BMI valueBMI category
< 18.5Underweight
between 18.5 and 25 inclusiveHealthy weight
between 25.0 and 30 inclusiveOverweight
> 30Obesity

The table and the equation are taken from CDC website

To implement the calculator, create a new package named calculator inside the package spockdemo. You’ll need to create two classes inside the calculator package. Create one class named BMICategory and replace the existing code with the following:

package com.example.spockdemo.calculator;

public enum BMICategory {
    UNDERWEIGHT,
    HEALTHY_WEIGHT,
    OVERWEIGHT,
    OBESITY
}

Create the second class named BMICalculator and replace the existing code with the following:

package com.example.spockdemo.calculator;

import static com.example.spockdemo.calculator.BMICategory.*;

public class BMICalculator {

    public static BMICategory calculate(double weight, double height) {

        final var bmiValue = (weight / height / height) * 10000;

        if (bmiValue < 18.5) {
            return UNDERWEIGHT;
        }

        if (bmiValue <= 25) {
            return HEALTHY_WEIGHT;
        }

        if (bmiValue <= 30) {
            return OVERWEIGHT;
        }

        return OBESITY;
    }
}

Create the Test Class

Now it’s time to create the test class for the target class. To create a test class, right-click the BMICalculator class name within the code and go to the Generate… option (or press alt + insert if you are using the default configuration of IntelliJ). Choose the Test… option.

Another window will pop up to set some test parameters. Set the same values as shown in the image below.

Spock test parameters

After setting the test parameters, another window will pop up to choose the destination directory of the test. Set the directory to the same groovy directory created before.

Implement the Unit Test

Unit tests created using the Spock framework have a structure to follow. That structure is similar to the structure defined in Behavior Driven Development (BDD). The idea is to use given, when, and then clauses to divide the test parts. Here’s a brief explanation of each one:

  • given: The definition of the input test data.
  • when: Here is where I call the target method.
  • then: The assertions of the output values.

Let’s try one example to get used to the parts mentioned above. Replace the BMICalculatorTest content with the following:

package com.example.spockdemo.calculator

import spock.lang.Specification

import static com.example.spockdemo.calculator.BMICategory.*

class BMICalculatorTest extends Specification {
    def "given height=186 and weight=82 then return HEALTHY_WEIGHT"() {

        given:
        def height = 186
        def weight = 82

        when:
        def result = BMICalculator.calculate(weight, height)

        then:
        result == HEALTHY_WEIGHT
    }
}

Here’s what is going on in the above code:

  1. Any Spock test class should inherit from the Specification class to work properly. That’s what the extends keyword at the class name does.
  2. Using the ' def ' keyword, I’ve defined variables to hold the height and weight inside the given clause.
  3. Inside the when clause, I call the method under test using weight and height.
  4. I validate the result variable with the expected output inside the' then' clause.

You can run the test by clicking the green play button at the left of the BMICalculatorTest class. This test will run just fine. However, you may notice that the code tests just one scenario when the expected output is HEALTHY_WEIGHT. A good test should assert all the possible results from the target class.

Instead of copying and pasting the whole structure to another test and changing the input and expected values, you can use the data-driven testing tools available in Spock.

Unit tests in Spock use two features to be a data-driven test. These features are the where clause and the @Unroll annotation. Here’s a brief explanation:

  • The where clause defines the input and the expected output in a tabular form.
  • The @Unroll annotation replaces variables starting with # in the method name with the variables in the where clause.

Let’s see those features in action. Replace the content in BMICalculatorTest with the following:


package com.example.spockdemo.calculator

import spock.lang.Specification
import spock.lang.Unroll

import static com.example.spockdemo.calculator.BMICategory.*

class BMICalculatorTest extends Specification {

    @Unroll
    def "given height=#height and weight=#weight then return #expected"() {

        when:
        def result = BMICalculator.calculate(weight, height)

        then:
        result == expected

        where:
        height | weight | expected
        186     | 82       | HEALTHY_WEIGHT
        150     | 82       | OBESITY
        175     | 82       | OVERWEIGHT
        190     | 65       | UNDERWEIGHT
    }
}

Here’s what is going on here:

  • The given clause is not mandatory, so I deleted it. The test data now lives in the where clause.
  • Right above the method name, I’ve put the @Unroll annotation. That annotation replaces the #height, #weight, and #expected strings with the same variables defined in the where clause.
  • For each line in the where clause table, Spock executes a new test with the data contained in each column.

Notice how the input and expected values form a table in the where clause of the unit test. Spock shines when testing methods where we can write their input and expected results in a tabular form, like in the BMICalculatorTest class.

In just one method, we tested all possible scenarios of our BMI calculator. See how straightforward and clear that method is. The tabular format of the where clause clarifies the input variables and the expected output of the calculate() method.

Run the Unit Tests

Click on the green play button in the BMICalculatorTest class to run the tests. You can also run the unit tests on your project by clicking on the test button in the Maven menu, located at the top right corner of IntelliJ. You should get the following results:

Unit test reports of Spock framework
Spock transformed each line in the where clause into another test. Also, the variables in where were replaced in the method name because of the @Unroll annotation.

Writing nice-to-read test names is good practice for generating good test reports. Using Spock Data-Driven Test features, we can easily achieve that.

What’s next for using the Spock framework?

Unit tests are the best way to test the basic functionalities of software applications. In the test pyramid, they compose about 75% of all tests. Thus, unit tests are created all the time by developers. If you have a framework that lets developers create less test code, you increase their productivity. Spock does that job very well.

As shown in this article, Spock provides a way to create easy-to-follow data-driven tests. Spock divides the test using the given/when/then approach, which makes it easier to read. People that glance at this test code have a good idea about what each clause does. Thus, Spock could also increase the maintainability of the application.

In this post, I’ve demonstrated how to create data-driven unit tests using Spock. For the next steps, I’d highly encourage you to:

  • Check the Spock documentation. Spock has many other valuable features like Mocking, Interaction-based testing, and utility classes. The documentation provided by the authors is just like Groovy syntax: concise and objective.
  • Glance at the various flavors of tests and the importance of each in this blog post by Martin Fowler. Knowing how to create better unit tests and read their reports can make software engineering more productive and fun!
  • Unit tests are handy for software applications that adopt Continuous Integration, Test Driven Development, and Refactoring. Those concepts are prevalent in the current software engineering industry. You might want to check them too!

I’d love to see what you create using Spock!

Pedro Lopes is a backend developer and independent writer. He's a specialist and enthusiastic about distributed systems, big data, and high-performance computing. He can be found on LinkedIn, Github, and his Blog.