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'
    }
40
demandé sur René Scheibe 2013-10-04 19:54:37

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

}
32
répondu Peter Niederwieser 2016-06-27 10:33:56

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
        }
    }
5
répondu Amanuel Nega 2015-02-18 08:17:47

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:

  1. vous avez besoin de plusieurs blocs de capture pour tester différentes exceptions.
  2. Vous devez utiliser des conditions explicites (assert statements) à l'intérieur des blocs try/catch.
  3. vous ne pouvez pas séparer votre stimulus et les réponses en when-then blocs.
4
répondu Ben Cass 2016-02-17 19:13:25

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
        }
    }
}
​
3
répondu Duncan 2015-02-21 06:25:57

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

1
répondu idij 2017-07-19 16:23:19

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' | _
    }
}
0
répondu mkobit 2016-01-14 19:04:11