Spock-test Exceptions with Data Tables
comment les exceptions peuvent-elles être testées d'une manière agréable (par exemple, tables de données) avec Spock?
Exemple: Avoir une méthode validateUser qui peut lancer des exceptions avec des messages différents ou pas d'exception si l'utilisateur est valide.
la classe de spécification elle-même:
class User { String userName }
class SomeSpec extends spock.lang.Specification {
...tests go here...
private validateUser(User user) {
if (!user) throw new Exception ('no user')
if (!user.userName) throw new Exception ('no userName')
}
}
variante 1
celui-ci fonctionne mais l'intention réelle est encombrée par tous les quand/ les étiquettes et les appels répétés de validateUser(user).
def 'validate user - the long way - working but not nice'() {
when:
def user = new User(userName: 'tester')
validateUser(user)
then:
noExceptionThrown()
when:
user = new User(userName: null)
validateUser(user)
then:
def ex = thrown(Exception)
ex.message == 'no userName'
when:
user = null
validateUser(user)
then:
ex = thrown(Exception)
ex.message == 'no user'
}
variante 2
celui-ci ne fonctionne pas à cause de cette erreur soulevée par Spock au moment de la compilation:
des conditions d'Exception ne sont autorisés que dans", puis " blocs
def 'validate user - data table 1 - not working'() {
when:
validateUser(user)
then:
check()
where:
user || check
new User(userName: 'tester') || { noExceptionThrown() }
new User(userName: null) || { Exception ex = thrown(); ex.message == 'no userName' }
null || { Exception ex = thrown(); ex.message == 'no user' }
}
variante 3
celui-ci ne fonctionne pas à cause de cette erreur soulevée par Spock au moment de la compilation:
les conditions D'Exception ne sont autorisées qu'au niveau supérieur déclarations
def 'validate user - data table 2 - not working'() {
when:
validateUser(user)
then:
if (expectedException) {
def ex = thrown(expectedException)
ex.message == expectedMessage
} else {
noExceptionThrown()
}
where:
user || expectedException | expectedMessage
new User(userName: 'tester') || null | null
new User(userName: null) || Exception | 'no userName'
null || Exception | 'no user'
}
6 réponses
la solution recommandée est d'avoir deux méthodes: une qui teste les bons cas, et une autre qui teste les mauvais cas. Les deux méthodes peuvent alors utiliser des tableaux de données.
Exemple:
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')
}
}
Vous pouvez envelopper votre appel de méthode avec une méthode qui renvoie le message ou la classe exception, ou une carte des deux...
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
}
}
Voici la solution que j'ai trouvé. C'est en gros la variante 3, mais elle utilise un try/catch bloquer pour éviter d'utiliser les conditions d'exception de Spock (depuis ceux niveau le plus haut).
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'
}
Quelques mises en garde:
- vous avez besoin de plusieurs blocs de capture pour tester différentes exceptions.
- Vous devez utiliser des conditions explicites (
assertstatements) à l'intérieur des blocs try/catch. - vous ne pouvez pas séparer votre stimulus et les réponses en
when-thenblocs.
en utilisant l'exemple de @AmanuelNega j'ai essayé cela sur la console Web de spock et j'ai sauvegardé le code à http://meetspock.appspot.com/script/5713144022302720
import spock.lang.Specification
class MathDemo {
static determineAverage(...values)
throws IllegalArgumentException {
for (item in values) {
if (! (item instanceof Number)) {
throw new IllegalArgumentException()
}
}
if (!values) {
return 0
}
return values.sum() / values.size()
}
}
class AvgSpec extends Specification {
@Unroll
def "average of #values gives #result"(values, result){
expect:
MathDemo.determineAverage(*values) == result
where:
values || result
[1,2,3] || 2
[2, 7, 4, 4] || 4.25
[] || 0
}
@Unroll
def "determineAverage called with #values throws #exception"(values, exception){
setup:
def e = getException(MathDemo.&determineAverage, *values)
expect:
exception == e?.class
where:
values || exception
['kitten', 1]|| java.lang.IllegalArgumentException
[99, true] || java.lang.IllegalArgumentException
[1,2,3] || null
}
Exception getException(closure, ...args){
try{
closure.call(args)
return null
} catch(any) {
return any
}
}
}
Voici comment je le fais, je modifie le when: l'article de toujours jeter un Success exception, de cette façon vous n'avez pas besoin de tests séparés ou de logique pour dire s'il faut appeler thrown ou notThrown, il suffit de toujours appeler thrown avec la table de données indiquant s'il faut s'attendre à Success ou pas.
Vous pouvez renommer SuccessNone ou NoException ou ce que vous préférez.
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')
}
}
une chose supplémentaire que je changerais, serait d'utiliser une sous-classe pour les exceptions d'échec aussi pour éviter un Success se faire attraper accidentellement alors qu'on s'attendait à un échec. Il n'affecte pas votre exemple parce que vous avez un contrôle supplémentaire pour le message, mais d'autres tests pourraient juste tester le type d'exception.
class Failure extends Exception {}
et de l'utiliser ou d'une autre "vraie" exception au lieu de la vanille Exception
voici un exemple de la façon dont je l'ai réalisé en utilisant @Unroll et when:, then: et where: blocs. Il fonctionne en utilisant les 3 tests avec les données de la table de données:
import spock.lang.Specification
import spock.lang.Unroll
import java.util.regex.Pattern
class MyVowelString {
private static final Pattern HAS_VOWELS = Pattern.compile('[aeiouAEIOU]')
final String string
MyVowelString(String string) {
assert string != null && HAS_VOWELS.matcher(string).find()
this.string = string
}
}
class PositiveNumberTest extends Specification {
@Unroll
def "invalid constructors with argument #number"() {
when:
new MyVowelString(string)
then:
thrown(AssertionError)
where:
string | _
'' | _
null | _
'pppp' | _
}
}