Spock Test tricks

1/11/2013

The Spock test framework is a neat way to create concise unit tests with built-in mocking and stubbing support. Its documentation was always somewhat lacking, though, and the more interesting use cases were not part of the official docs. This seems to be changing with the new reference documentation currently in the works, but I hope these tricks my team discovered over time might still be useful.

Matchers and arguments

When you expect a specific argument to be passed to a mock, you have two very good options to check them:

  1. Simple types: put the expected value inline, like 1 * mock.method('expectedValue').
  2. Pre-constrcut the exact object that you expect, and make sure it has a working equals() method: 1 * mock.method(myPreConstrcutedObject)

If you expect some complex object to be passed, but you don't control the class or you only care about a few of its properties, you're out of luck, and need to use a matcher.

1 * mock.method({ MyType arg ->
    arg.property1 == 'expectedValue1' && arg.property2 == 'expectedValue2'
})

The matcher matches the arguments if it evaluates to true, and does not if it evaluates to false. Simple enough, but what if you have more than a handful of properties, or the check logic is more complex?

We ended up having quite a number of matchers that we mis-use using assert statements:

1 * mock.method({ MyType arg ->
    assert arg.property1 == 'expectedValue1'
    assert arg.property2 == 'expectedValue2'
    //more complex logic
    if (...) {
        assert ...
    }
    true
})

This works quite nicely, as long as you remember to return true on the last line of the matcher method, and as long as you don't forget any of the asserts – a matcher is a single expression, not a series of expressions.

Stubbing and arguments

Every once in a while, you want to return something from a method call based on the actual arguments. This is simple enough to do:

mock.method(_) >> { MyType arg ->
   return arg != null? arg.myProperty : null
}

This is especially useful for DAO mocks returning the argument itself:

daoMock.merge(_) >> { MyEntity arg ->
   return arg
}

Mocking builders

Builders have the problem that all their methods except the build() method need to return the builder itself, making it quite tiresome to mock.

def builderResult = Mock(Mail)
def mailBuilder = Mock(Mail.Builder)
mailBuilder.build() >> builderResult
mailBuilder.attachment(_) >> mailBuilder
mailBuilder.from(_) >> mailBuilder
....

Luckily, you can mock multiple methods at once:

mailBuilder./attachment|from|to|cc|bcc|template/(*_) >> mailBuilder
mailBuilder.build() >> builderResult

This should save you from a lot of typing around builders.

Test setup using instance variables

Initially, our tests had most of their repeating initialization in their setup() methods:

class SomeTestClass {
    private SomeClass someMock
    private ClassUnderTest underTest

    def setup() {
        this.someMock = Mock(SomeClass)
        this.underTest = new ClassUnderTest(someProperty: someMock)
    }

    ...
}

This can be eliminated by using instance variables instead:

class SomeTestClass {
    def someMock = Mock(SomeClass)
    def underTest = new ClassUnderTest(someProperty: someMock)

    ...
}

The instance variables are re-initialized before every test case.

Overriding mock behavior

Sometimes, you have a mock instance that needs to behave the same way, except in one special case, so you might try something like this:

class SomeTestClass {
    def someMock = Mock(SomeClass)

    def setup() {
        someMock.method() >> 'returnValue'
    }

    def "test case failing to override mock behavior"() {
        setup:
        someMock.method() >> 'otherValue' //does not work

        when:
        ....
        then:
        ...
    }
}

This does not work, though, the bahavior initially specified still counts. You can, however, specify different behavior in the then: block:

class SomeTestClass {
    def someMock = Mock(SomeClass)

    def setup() {
        someMock.method() >> 'returnValue'
    }

    def "test case overriding mock behavior"() {
        when:
        ....
        then:
        1 * someMock.method() >> 'otherValue' //does actually work
        ...
    }
}

Comments