ONLINE TESTS EDITING BOARD
RO
EN
×
▼ BROWSE ISSUES ▼
Issue 60

Unit tests with Spock

Bianca Leuca
Software Developer @ Mozaic Labs
PROGRAMMING

Unit tests with Spock

Why Spock?

When we first started working on our project at work, we searched for an easy-to-use tool that would help us write readable and concise tests. We chose Spock, because in combination with Groovy it provided exactly what we needed: readable BDD-style tests that we could write fast and change easily. Keep in mind that Spock can also be used in combination with Java, but has a great synergy with Groovy.

All the examples in this article are written in Groovy, but fear not! If you are a little bit familiar with Java, you will see that you can read Groovy code. Because our project's tests examples would have been too complicated for a Spock introduction, I have chosen examples from a kata I coded to practice TDD.

Getting started

Adding Spock with Gradle is easy. This is my build.gradle file:

apply plugin: 'groovy'
repositories {
  mavenCentral()
}
dependencies {
  testCompile(
    'junit:junit:4.12',
    'org.codehaus.groovy:'+
       'groovy-all:2.4.4',
    'org.spockframework:
       'spock-core:1.0-groovy-2.4',
    )
    testRuntime(
    // for spock reports
    'com.athaydes:'
    +'spock-reports:1.2.7'
    )
}

Write the first falling test:

import spock.lang.Specification
class SeeIfItWorksSpec extends 
 Specification {
   def "it should fail"() {
      expect:
      false
   }
}

The output is:

Condition not satisfied:

false

Afterwards, replace false with true. If the output is "Tests PASSED", your setup is complete.

Spock syntax

Naming

Test names in Spock are written as String literals. This allows you to write full phrases that help with test readability and should make you understand what the test is about just by reading the name.

def "transforms 1 into roman
  numeral I and writes the result   
  into a file"() {

}

Blocks

By extending Spock's base class, Specification, you are given access to the six types of blocks into your tests (given, when, then, expect, where, cleanup). With their help, you have a clear separation of the preconditions, the action that you are testing, and the response.

Example:

I chose as an example an arabic to roman numeral transformer test:

class NumeralTransformerSpec extends Specification {
  def numeralTransformer

  def setup() {
    numeralTransformer = 
      new NumeralTransformer()
   }

   def "transforms arabic numeral 1 into roman
      numeral I"() {
      given:
      def arabicNumeral = 1

      when:
      def result = numeralTransformer
         .transform(arabicNumeral)

      then:
      result == "I"
   }
}

The example above reads as follows: given an arabicNumeral, when the method transform is called with the arabicNumeral, then the expected result is "I".

In the setup method you put the code that runs before every test method. In this example, the object under test is initialized there to avoid duplication in other test methods.

As a side note, you might have noticed the def keyword. In Groovy, you can declare a variable or a method with def, which means that you do not restrict its type. It is similar to declaring it as an Object.

Given Block

The setup work for the feature you are testing is placed in the given block. This can contain initialization of the object you are testing, mocks and stubs, declaration of variables, etc.

When and Then Blocks

These blocks are always together. Here you specify what you are testing and check its response. While the given block is optional, these blocks are always present, unless you decide to use the expect block.

The code in the then block can check for three things: the state of the result, if any exceptions are thrown or if there are interactions with other classes or methods.

Checking the state

The code that checks the state in Spock is similar with Junit's assertions, but here you can just use plain boolean conditions.

As an example, let's say the previous NumeralTransformer class has a method that maps the arabic numeral to its roman representation and returns the map.

def "builds the numeral mapping for 1"() {
   when:
   def result = numeralTransformer.buildNumeralMap(1)
   then:
   result == [1: "I"]
}

What happens if we change the state condition?

def "builds the numeral mapping for 1""() {
   when:
   def result = numeralTransformer.buildNumeralMap(1)
   then:
   result == [1: "II"]
}

The output of failing conditions looks like this:

Failure:  builds the numeral mapping(com.coolconf.unitTestsWithSpock.RomanTransformerSpec)
Condition not satisfied:
result  ==  [1: "II"]
   |     |
  [1:I] false

Checking exceptions

Checking that an exception is thrown in the then block is done by using the thrown method with the expected exception type as parameter. In the following example, an IllegalArgumentException is expected to be thrown when the number provided as parameter is -1:

def "throws IllegalArgumentException if
   number is -1"() {
   when:
   numeralTransformer.transform(-1)

   then:
   thrown(IllegalArgumentException)
}

Checking interactions

Checking interactions means verifying the way the object under specification interacts with other objects. This is done using Mock objects, provided by Spock's MockingApi.

In this section I will only give you an example, we will explore mock objects' behaviour in the Mocks and Stubs section.

class NumeralTransformerSpec extends Specification {
   def numeralTransformer

   def setup() {
      numeralTransformer = new NumeralTransformer()
      numeralTransformer.fileWriterService = 
         Mock(FileWriterService)
   }

   def "transforms 1 into roman numeral I and writes
      the result into a file"() {
      given:
      def arabicNumeral = 1
      def file = new File("/somepath")

      when:
      numeralTransformer
        .writeTheRomanTransformationToFile(file, 
         arabicNumeral)

      then:
      1 * numeralTransformer
        .fileWriterService.writesNumberToFile(file, 
         "I")
   }
}

In the above example, the Mock object FileWriterService is created in the setup section, and the interactions are verified in the then block.

The example reads as: given an arabic numeral with value "1" and a file, when the writeTheRomanTransformationToFile method is called with the arabic numeral and the file as params, then the fileWriterService's methodwritesNumberToFile()is called exactly once with the file and the "I"String` as params.

Expect block

You can use this block when the action and result make more sense to be in a single expression. This block can only be used when checking a condition. It can also contain variable definitions.

You can rewrite one of the above examples as:

def "transforms 1 into roman value I"() {
   expect:
   numeralTransformer.transform(1) == "I"
}

Where block

This is useful when you have a data driven test, meaning you want to test a method for different inputs and check the results.

The where block allows you to run the test with different inputs, without duplicating it.

def "transforms arabic numeral into roman numeral"() {
   when:
   def result = numeralTransformer.
     transform(arabicNumeral)

   then:
   result == expectedResult

   where:
   arabicNumeral || expectedResult
   1             || "I"
   2             || "II"
   3             || "III"
}

In this example, the where block contains a data table. The table header represents the data variables, and the rest of the table rows are their values. Each row represents a test case, and for each of them the test will be executed once, with the setup method at the beginning of each iteration and the cleanup method at the end (if they are present). The output of the test can be separated by two pipes.

Unroll annotation

@Unroll
def "transform arabic numeral #arabicNumeral into
   roman numeral #expectedResult"() {
   when:
   def result = numeralTransformer
     .transform(arabicNumeral)

   then:
   result == expectedResult
   where:
   arabicNumeral || expectedResult
   1             || "I"
   2             || "II"
   3             || "III"
}

The purpose the Unroll annotation is to make the iterations be reported independently. This is especially useful when some of them fail and you need to know which one. In combination with placeholders (the variables in the test method with the leading "#"), it provides a readable report:

--Output from transform arabic numeral 1 into roman value I--| Running 1 unit test... 2 of 2

--Output from transform arabic numeral 2 into roman value II--| Running 1 unit test... 3 of 3

--Output from transform arabic numeral 3 into roman value III--| Completed 3 unit tests, 0 failed in 0m 0s

Data pipes

Data tables are just syntactic sugar for data pipes. The example above can be re-written as:

@Unroll
def "transform arabic numeral #arabicNumeral into
   roman numeral #expectedResult"() {
   when:
   def result = numeralTransformer
     .transform(arabicNumeral)

   then:
   result == expectedResult

   where:
   arabicNumeral << [1, 2, 3]
   expectedResult << ["I", "II", "III"]
}
  1. Mocks and Stubs

Mocking in Spock is lenient, meaning that any method calls on the mocked object that are not relevant to the tests and (therefore their interactions are not checked in the specification) are allowed and answered with a default response(null, false or zero).

Mock objects have no behaviour and should not be confused with stubs.

Interactions

Let's revisit the example with the fileWriterService:

then:

then:
  1 * numeralTransformer.fileWriterService
    .writesNumberToFile(file, "I")

In the interaction verification it is specified:

Declaring interactions placement

I most often declare interactions in the then block, but this is not mandatory. This can be done anywhere before the when block, meaning you can place them in the setup method or in the given block.

Also, you can declare them at mock time creation or afterwards.

Failing interactions

What happens when an interaction fails? It can fail for two main reasons: either the number of interactions specified is wrong, or the arguments of the called method are wrong.

I will use the example above and change the cardinality of the interaction:

def "transforms 1 into roman numeral I and writes the result into a file"() {
      given:
      def arabicNumeral = 1
      def file = new File("/somepath")

      when:
      numeralTransformer
        .writeTheRomanTransformationToFile(file, 
         arabicNumeral)

      then:
      0 * numeralTransformer
        .fileWriterService.writesNumberToFile(file, 
         "I")
}

The error shown is:

Too many invocations for:

0 * numeralTransformer.fileWriterService.writesNumberToFile(file, "I") (1 invocation)

Matching invocations (ordered by last occurrence):

1 * .writesNumberToFile(/somepath, 'I') <-- this triggered the error

The error is pretty self explanatory: you have one invocation of writeTheRomanTransformationToFile method on the mocked FileWriterService in the production code, and the test expects zero.

At the opposite, checking

2 * **numeralTransformer**.fileWriterService.writesNumberToFile(file, **"I"**)

will trigger the following error:

Too few invocations for:

2 * numeralTransformer.fileWriterService.writesNumberToFile(file, "I") (1 invocation)

This means there should have been two invocations for the service call, but there is only one.

Stubbing

Stubbing means making a collaborator act in a certain way (return a value or a sequence of values, throw an error).

Example:

class RomanNumeralsArithmeticsSpec extends 
 Specification {
   def romanNumeralsArithmeticsService

   def setup() {
      romanNumeralsArithmeticsService = 
        new RomanNumeralsArithmeticsService()

      romanNumeralsArithmeticsService
       .numeralTransformerService =                 Mock(NumeralTransformerService)
   }

   def "check I + II = III"() {
     given:
     def first = "I"
     def second = "II"

     and:
     romanNumeralsArithmeticsService
      .numeralTransformerService.fromRoman(first) >> 1

    romanNumeralsArithmeticsService
    .numeralTransformerService.fromRoman(second) >> 2

    romanNumeralsArithmeticsService
    .numeralTransformerService.transform(3) >> "III"

    when:
    def sum = romanNumeralsArithmeticsService
    .sum(first, second)

     then:
     sum == "III"
   }
}

In this example, the sum of two roman numerals is checked. The numeralTranformerService is stubbed and its methods are made to return certain values that are further used in the production code.

The example reads as follows: given two roman numerals, and the methods RomanNumeralsArithmeticsService's collaborator are returning 1, 2 and "III", then the sum is "III".

Combining checking interactions with stubbing

You can use stubbing and checking interactions at the same time and you can group the methods of the same mocked object:

  def "check I + II = III"() {
      given:
      def first = "I"
      def second = "II"
      romanNumeralsArithmeticsService
       .numeralTransformerService =                 Mock(NumeralTransformerService) {
         1 * fromRoman(first) >> 1
         1 * fromRoman(second) >> 2
         1 * transform(3) >> "III"
      }

      when:
      def sum = romanNumeralsArithmeticsService
       .sum(first, second)

      then:
      sum == "III"
   }
}
  1. Spock Reports

Spock allows you to write textual descriptions to blocks:

@Unroll
def "transforms arabic numeral #arabicNumeral
   into roman numeral #expectedResult"() {
   when: "Calls transform method on arabic numeral"
   def result = numeralTransformer
     .transform(arabicNumeral)

   then: "Expected result is '#expectedResult'"
   result == expectedResult

   where: "Arabic numeral is #arabicNumeral"
   arabicNumeral || expectedResult
   1             || "I"
   2             || "II"
   3             || "III"
}

Thanks to Spock-Reports-Plugin, you can generate human readable reports that allow you to use your specifications as documentation. You can add the plugin simply by adding this in your build.gradle file:

Report for NumeralTransformerSpec

Summary:

Created on Fri May 05 22:10:34 EEST 2017 by biancal

+-------------------+----------+--------+--------------+-----------+
| Executed features | Failures | Errors | Success rate | Time      |
+-------------------+----------+--------+--------------+-----------+
| 3                 | 0        | 0      | 100.0%       | 0.074 sec |
+-------------------+----------+--------+--------------+-----------+

Features:

transforms arabic numeral 1 into roman numeral I

transforms arabic numeral 2 into roman numeral II

transforms arabic numeral 3 into roman numeral III

+------------------------------------------+------------------------------------------+
| transforms arabic numeral 1 into roman   |                                          |
| numeral I                                |                                          |
+------------------------------------------+------------------------------------------+
| When:                                    | Calls transform method on arabic numeral |
+------------------------------------------+------------------------------------------+
| Then:                                    | Expected result is 'I'                   |
+------------------------------------------+------------------------------------------+
| Where:                                   | Arabic numeral is 1                      |
+------------------------------------------+------------------------------------------+
| transforms arabic numeral 2 into roman   |                                          |
| numeral II                               |                                          |
+------------------------------------------+------------------------------------------+
| When:                                    | Calls transform method on arabic numeral |
+------------------------------------------+------------------------------------------+
| Then:                                    | Expected result is 'II'                  |
+------------------------------------------+------------------------------------------+
| Where:                                   | Arabic numeral is 2                      |
+------------------------------------------+------------------------------------------+
| transforms arabic numeral 3 into roman   |                                          |
| numeral III                              |                                          |
+------------------------------------------+------------------------------------------+
| When:                                    | Calls transform method on arabic numeral |
+------------------------------------------+------------------------------------------+
| Then:                                    | Expected result is 'III'                 |
+------------------------------------------+------------------------------------------+
| Where:                                   | Arabic numeral is 3                      |
+------------------------------------------+------------------------------------------+

The specification as a documentation feature that Spock provides is especially useful for higher-level features that can be read by a other members of the team that are not programmers (for example, product owners).

Conclusion

If you are working with Groovy or Java and are looking for a testing framework that has an easy learning curve, it generates readable BDD style reports and has a lenient mocking library that does not clutter your code with irrelevant interaction checking, I suggest looking into the Spock framework.

Sponsors

  • ntt data
  • 3PillarGlobal
  • Betfair
  • Telenav
  • Accenture
  • Siemens
  • Bosch
  • FlowTraders
  • MHP
  • BCR
  • Itiviti
  • Connatix
  • UIPatj
  • MicroFocus
  • Colors in projects