Comment les exceptions peuvent-elles être testées de manière agréable (tables de données, par exemple) avec Spock?
Exemple: Avoir une méthode validateUser
pouvant générer des exceptions avec des messages différents ou aucune 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 toutes les étiquettes when/then 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 lors de la compilation:
Les conditions d'exception ne sont autorisées que dans les blocs 'then'
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 lors de la compilation:
Les conditions d'exception ne sont autorisées que comme instructions de niveau supérieur
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'
}
La solution recommandée consiste à utiliser deux méthodes: une qui teste les bons cas et une autre qui teste les mauvais cas. Ensuite, les deux méthodes peuvent utiliser des tables 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 retourne le message ou la classe d'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ée. Il s’agit essentiellement de la variante 3, mais elle utilise un bloc try/catch
pour éviter d’utiliser les conditions d’exception de Spock (puisque ces doivent obligatoirement être de niveau supérieur).
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:
assert
) à l'intérieur des blocs try/catch.when-then
.En utilisant l’exemple de @AmanuelNega, j’ai essayé cette solution sur la console Web de Spock et ai enregistré le code à l’adresse 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 la clause when:
pour toujours générer une exception Success
. Ainsi, vous n'avez pas besoin de tests ni de logique distincts pour indiquer s'il faut appeler thrown
ou notThrown
, il suffit d'appeler toujours thrown
avec la table de données indiquant s'il faut s'attendre à Success
ou non.
Vous pouvez renommer Success
pour être None
ou NoException
ou comme 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 afin d'éviter qu'une Success
ne soit accidentellement interceptée alors que vous vous attendiez vraiment à un échec. Cela n’affecte pas votre exemple, car le message contient une vérification supplémentaire, mais d’autres tests pourraient simplement tester le type d’exception.
class Failure extends Exception {}
et utiliser cela ou une autre exception "réelle" à la place de Vanilla Exception
Voici un exemple de la façon dont je l'ai réalisé en utilisant @Unroll
et les blocs when:
, then:
et where:
Il utilise les trois tests avec les données du tableau 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' | _
}
}