Spock - Testing Exceptions with Data Tables

The recommended solution is to have two methods: one that tests the good cases, and another that tests the bad cases. Then both methods can make use of data tables.

Example:

class SomeSpec extends Specification {

    class User { String userName }

    def 'validate valid user'() {
        when:
        validateUser(user)

        then:
        noExceptionThrown()

        where:
        user << [
                new User(userName: 'tester'),
                new User(userName: 'joe')]
    }

    def 'validate invalid user'() {
        when:
        validateUser(user)

        then:
        def error = thrown(expectedException)
        error.message == expectedMessage

        where:
        user                     || expectedException | expectedMessage
        new User(userName: null) || Exception         | 'no userName'
        new User(userName: '')   || Exception         | 'no userName'
        null                     || Exception         | 'no user'
    }

    private validateUser(User user) {
        if (!user) throw new Exception('no user')
        if (!user.userName) throw new Exception('no userName')
    }

}

Here is the solution I came up with. It's basically Variant 3, but it uses a try/catch block to avoid using Spock's exception conditions (since those have to be top level).

def "validate user - data table 3 - working"() {
    expect:
    try {
        validateUser(user)
        assert !expectException
    }
    catch (UserException ex)
    {
        assert expectException
        assert ex.message == expectedMessage
    }

    where:
    user                         || expectException | expectedMessage
    new User(userName: 'tester') || false           | null
    new User(userName: null)     || true            | 'no userName'
    null                         || true            | 'no user'
}

Some caveats:

  1. You need multiple catch blocks to test different exceptions.
  2. You have to use explicit conditions (assert statements) inside of try/catch blocks.
  3. You can't separate your stimulus and responses into when-then blocks.

You can wrap your method call with a method that returns the message or the exception class, or a map of both...

  def 'validate user - data table 2 - not working'() {
        expect:
            expectedMessage == getExceptionMessage(&validateUser,user)
        where:
        user                         || expectedMessage
        new User(userName: 'tester') || null
        new User(userName: null)     || 'no userName'
        null                         || 'no user'
    }

    String getExceptionMessage(Closure c, Object... args){
        try{
            return c.call(args)
            //or return null here if you want to check only for exceptions
        }catch(Exception e){
            return e.message
        }
    }

Here's how I do it, I modify the when: clause to always throw a Success exception, that way you don't need separate tests or logic to tell whether to call thrown or notThrown, just always call thrown with the data table telling whether to expect Success or not.

You could rename Success to be None or NoException or whatever you prefer.

class User { String userName }

class SomeSpec extends spock.lang.Specification {

    class Success extends Exception {}

    def 'validate user - data table 2 - working'() {
        when:
            validateUser(user)
            throw new Success ()

        then:
            def ex = thrown(expectedException)
            ex.message == expectedMessage

        where:
            user                         || expectedException | expectedMessage 
            new User(userName: 'tester') || Success           | null
            new User(userName: null)     || Exception         | 'no userName'
            null                         || Exception         | 'no user'
    }

    private validateUser(User user) {
        if (!user) throw new Exception ('no user')
        if (!user.userName) throw new Exception ('no userName')
    }
}

One extra thing I would change, would be to use a subclass for the failure exceptions too to avoid a Success being accidentally caught when you were really expecting a failure. It doesn't affect your example because you have an extra check for the message, but other tests might just test the exception type.

class Failure extends Exception {}

and use that or some other "real" exception instead of the vanilla Exception