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.
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.
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"() {
}
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.
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.
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.
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 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 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 method
writesNumberToFile()is called exactly once with the file and the "I"
String` as params.
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"
}
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
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 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"]
}
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:
the cardinality ( 1 *) : the number of times the object under specification is expected to interact with the collaborator's method
the target constraint (fileWriterService): is the mocked object
the method constraint (writesNumberToFile) : the method that it is expected to be called
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.
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 * <FileWriterService>.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 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".
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"
}
}
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).
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.