Spock – Sharing conditions across then blocks

Using helper methods with the Spock specification framework helps avoid duplicating conditions across multiple tests

We often find ourselves applying the same bits of validation logic across multiple tests in a test suite (sometimes even across test suites). When working with a plain Java framework, such as JUnit, we would instinctively look to factor those conditions out into a common helper method.

Frameworks like Spock, which combines the lighter syntax of Groovy with a clean means to express our test conditions help to make our tests significantly more clear and readable. But what to do with the cut-and-paste common conditions we’d normally factor out of our individual tests?

Well fortunately, since Spock is Groovy based and Groovy is a fully-fledged programming language, we can do exactly the same thing we’d do with a JUnit test.

A simple Spock test case

By way of an astonishingly simple example, let’s say we’re putting some tests together for a Rectangle Java class, which currently exposes the following methods:-

public interface Rectangle {
    int getWidth();
    int getHeight();
    int getArea();
    void inflate(int percentage);
    void deflate(int percentage);
}

Currently it only lets us apply a couple of operations but we can expect that to grow in the future. Let’s start with a simple test for the inflate() method (we’ll ignore rounding issues and stick to whole numbers for now):-

class RectangleTest extends spock.lang.Specification {
    def "Inflate"() {
        when :
            def cut = new RectangleImpl(width: 100, height: 200)
            cut.inflate(pct)
        then :
            cut.width == newWidth
            cut.height == newHeight
            cut.area == cut.width * cut.height
        where:
            pct | newWidth | newHeight
            10  | 110      | 220
            25  | 125      | 250
            33  | 133      | 266
    }
}

Here we’re using a data table in a where block to pump a range of values into our Class Under Test and validate that the resulting values are what we’d expect. We’ve also thrown in a consistency test on the area for good measure. Our test specifies exactly the conditions we’re testing and little else in a clear, readable and concise form.

Adding more tests

With our first test running fine, we’ll now go on to add a test for the deflate() method:-

def "Deflate"() {
    when :
        def cut = new RectangleImpl(width: 100, height: 200)
        cut.deflate(pct)
    then :
        cut.width == newWidth
        cut.height == newHeight
        cut.area == cut.width * cut.height
    where:
        pct | newWidth | newHeight
        10  | 90       | 180
        25  | 75       | 150
        33  | 67       | 134
}

At this point we find a bug – our implementation class looks like it might have some dodgy divides in it – one for the developer to look at:-

Condition not satisfied:

cut.width == newWidth
|   |     |  |
|   0     |  90
|         false

In the meantime though, lets look at our tests. There’s not a lot we can do about the when and where blocks, but both of our then blocks are identical and the chances are that as more methods are added the same thing will happen again. A no-brainer case for some refactoring.

Factoring out common conditions

And fortunately with Spock we have a couple of options. First we can define a boolean function inside our test class which evaluates all our conditions:-

def validRectangle(rect, width, height) {
    rect.width == width &&
        rect.height == height &&
        rect.area == rect.width * rect.height
}

And change both of our then blocks to call it:-

    then :
        validRectangle(cut, newWidth, newHeight)

Unfortunately with this approach the messages for our failed tests become somewhat less descriptive:-

Condition not satisfied:

validRectangle(cut, newWidth, newHeight)
|              |    |         |
false          |    90        180

This is because the condition that failed is the function call itself and this doesn’t tell us anything about which part of the underlying expression didn’t hold true.

A better alternative is to move the individual conditions out to a function with no return value and have each condition evaluate separately. Our first attempt might look like this:-

void validRectangle(rect, width, height) {
    rect.width == width
    rect.height == height
    rect.area == rect.width * rect.height
}

Here we’ve simply copied our conditions across to our helper function and renamed the variables, but it’s not quite that simple. Since we’ve moved them out of the direct context of the then block they will no longer be implicitly asserted. Our previously failing test will now run successfully whatever numbers come back from our function calls.

When factoring conditions out in this way it’s important to remember to explicitly add the assert keyword into each test since it’s no longer implicit:-

void validRectangle(rect, width, height) {
    assert rect.width == width
    assert rect.height == height
    assert rect.area == width * height
}

Our test feature now returns to its previous behaviour, correctly failing when it encounters the deflate bug and with a useful error message:-

Condition not satisfied:

rect.width == width
|    |     |  |
|    0     |  75
|          false

Test specification frameworks like Spock can add a lot of value to our tests by more clearly separating specification from execution and thus make them much cleaner and more expressive. These benefits go out of the window though if we don’t look for ways of applying the same best practice approaches we’d be applying with more conventional testing tools, such as factoring out common logic to avoid maintenance nightmare cut-and-paste clutter.

Leave a Reply

Your email address will not be published. Required fields are marked *