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'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
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
}
}