IntelliJ IDEA
IntelliJ IDEA – the Leading Java and Kotlin IDE, by JetBrains
Tutorial: Spock Part 5 – Other Useful Tips
We’ve now covered all the key features for creating and running tests with Spock. In Part 5, we finish up the Spock tutorial by looking at additional features that can help you to write short, descriptive, correct tests.
Tutorial: Spock
- Part 1 – Getting Started
- Part 2 – Writing Tests
- Part 3 – Data Driven Testing
- Part 4 – Mocking and Stubbing
- Part 5 – Other Useful Tips
These blog posts cover the same material as the video. This provides an easy way for people to skim the content quickly if they prefer reading to watching, and to give the reader/watcher code samples and links to additional information.
Helper Methods
When tests get big, we may want to split out large parts of code, or common code, into helper methods.
Let’s say we have a test like this one.
def "should use a helper method"() {
given:
Renderer renderer = Mock()
def shapeFactory = new ShapeFactory(renderer)
when:
def polygon = shapeFactory.createDefaultPolygon()
then:
polygon.numberOfSides == 4
polygon.renderer == renderer
//could check lots of different values on this polygon...
}
It uses a ShapeFactory to create a default shape, and then we perform a number of checks to make sure this meets our expectations. You can imagine in real production code there might be a lot of values to check here.
We may be tempted to move all these checks into their own method, especially if they’re going to be used by more than one test.
def "should use a helper method"() {
//given... when... code
then:
checkDefaultShape(polygon, renderer)
}
private static void checkDefaultShape(Polygon polygon, Renderer renderer) {
polygon.numberOfSides == 4
polygon.renderer == renderer
}
Run the test – it will pass. However, if we change the code so it should fail, we’ll see that it still passes. This helper method is not doing what we expect.
If we move our assertions into a helper method like this, it can no longer use the comparison operators to define the expected behaviour. Instead, we need to add the assert
keyword specifically.
private static void checkDefaultShape(Polygon polygon, Renderer renderer) {
assert polygon.numberOfSides == 4
assert polygon.renderer == renderer
}
Now if you run the test with incorrect values in checkDefaultShape
, it should fail.
There’s something else to be aware of too – it fails on the first assertion that fails, it never runs the assertion to check the polygon’s renderer. Later we’ll look at how to address that.
with()
Let’s look at one approach to testing multiple properties of a single object. We can change the previous test to the following:
def "should use a helper method"() {
given:
Renderer mockRenderer = Mock()
def shapeFactory = new ShapeFactory(mockRenderer)
when:
def polygon = shapeFactory.createDefaultPolygon()
then:
with(polygon) {
numberOfSides == 4
renderer == null
}
}
We can use Spock’s with() and a closure to check multiple values on the polygon
. Inside this closure, we don’t have to say polygon.
, we just assert the property matches the expected value.
Note that in this test the mock Renderer created in the given
block is called mockRenderer
– this is so that it’s clear that the renderer
in the with
block is polygon.renderer
, not the renderer from the test scope.
Change the test so it fails, so we can see what this looks like:
As with the helper method, if the first assertion fails, it doesn’t run any further assertions. This might be what you want from your test, if one value is wrong the whole test should fail regardless. However, sometimes we want to run all the assertions so we can see exactly what’s working and what’s not.
verifyAll()
Let’s look at how to make sure all our assertions are run, regardless of whether one of them fails. Try this test (note that with string method names we can easily add quotes and other special characters).
def "should demonstrate 'verifyAll'"() {
given:
Renderer mockRenderer = Mock()
def shapeFactory = new ShapeFactory(mockRenderer)
when:
def polygon = shapeFactory.createDefaultPolygon()
then:
verifyAll(polygon) {
numberOfSides == 5
renderer == null
}
}
We can replace our with()
call with verifyAll() instead. Run this (the code above should fail) and see what happens – not only does the number of sides assertion fail, but the check on the renderer also fails.
org.opentest4j.MultipleFailuresError: Multiple Failures (2 failures) org.spockframework.runtime.SpockComparisonFailure: Condition not satisfied: numberOfSides == 5 | | 4 false org.spockframework.runtime.SpockComparisonFailure: Condition not satisfied: renderer == null | | | false Mock for type 'Renderer' named 'mockRenderer'
With verifyAll
, all assertions are run and we can see which fail and which pass. This can help us when we’re iterating quickly between writing and fixing tests.
Go back and fix this test:
then:
verifyAll(polygon) {
numberOfSides == 4
renderer == mockRenderer
}
(Note that this code differs slightly from the video, since using two variables called renderer
made it really hard to see what was being tested)
Setup and Teardown
If you’ve used other testing frameworks, the concept of test or specification class setup and tear down will be familiar.
Spock provides a setup
method (we can use IntelliJ IDEA’s code generation to create this), which will be run before every individual test method in this class. This can be used to set up a clean state at the start of each test.
To clean up data or state at the end of every test method, you need a cleanup
method. This is run after every individual test method.
Use the setupSpec
method to set up state once at the start of the specification, this is for things that should not change between individual test methods.
Create a cleanupSpec
method for final teardown code, this method will be called once at the very end of running all the tests.
One final piece of useful information. The tests in this tutorial created the “objects under test” in the test methods themselves. However, you might also want to create your test instance as a field in the test. You can use the @Subject annotation on the field to show that this is the object under test (you can use this annotation on local variables in the methods too). You can then reference this field in the test methods just as you’d expect in any Java or Groovy class.
@Subject
private Polygon polygon = new Polygon(5)
Specifications as Documentation
Let’s take a look at one last feature to help document the requirements via tests.
We’ve seen that Spock has a focus on readability and tests as documentation. The @Subject annotation, the labelled blocks, Strings as test method names plus all you can do to customise these String values all contribute to being able to use the automated tests as a documentation for what the system should do.
We can also add more information, again for readability or documentation purposes, to the blocks in our tests.
def "should be able to create a stub"() {
given: "a palette with red as the primary colour"
Palette palette = Stub()
palette.getPrimaryColour() >> Colour.Red
and: "a renderer initialised with the red palette"
def renderer = new Renderer(palette)
expect: "the renderer to use the palette's primary colour as foreground"
renderer.getForegroundColour() == Colour.Red
}
We can add a String next to the label to give a more detailed description of the block. If we want to split a block into further blocks, for readability or to add documentation, we can use the and: label, this is just to let us break things up further.
The text is available to the Spock runtime, so these messages can be used in messages and reports.
Conclusion
Spock is powerful and has even more to offer than we’ve looked at here.
Believe it or not we’ve only touched the surface of what Spock can offer. We’ve seen the basics of a test, we’ve seen how to use labels to define tests, we’ve seen the power of data driven testing, and we’ve covered a range of tips and tricks for writing correct and readable tests.
If you want to find out more about Spock, take a look at the excellent reference documentation. There’s also a Spock Primer which is a great place to start with Spock.