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 (
assert
statements) à l'intérieur des blocs try/catch. - vous ne pouvez pas séparer votre stimulus et les réponses en
when-then
blocs.
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 Success
None
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' | _
}
}