web-dev-qa-db-fra.com

Spock - Test des exceptions avec des tables de données

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'
    }
44
René Scheibe

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

}
37

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
        }
    }
5
Amanuel Nega

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:

  1. Vous avez besoin de plusieurs blocs catch pour tester différentes exceptions.
  2. Vous devez utiliser des conditions explicites (instructions assert) à l'intérieur des blocs try/catch.
  3. Vous ne pouvez pas séparer votre stimulus et vos réponses en blocs when-then.
4
Ben Cass

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
        }
    }
}
​
3
Duncan

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

1
idij

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' | _
    }
}
0
mkobit