wake-up-neo.net

Spock - Testen von Ausnahmen mit Datentabellen

Wie können Ausnahmen mit Spock auf nette Art getestet werden (z. B. Datentabellen)?

Beispiel: Eine Methode validateUser hat, die Ausnahmen mit unterschiedlichen Nachrichten oder ohne Ausnahme auslösen kann, wenn der Benutzer gültig ist.

Die Spezifikationsklasse selbst:

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

Dieser funktioniert, aber die eigentliche Absicht wird durch alle when/then - Labels und die wiederholten Aufrufe von validateUser(user) gestört.

    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

Dieser funktioniert nicht, da dieser Fehler von Spock beim Kompilieren ausgelöst wurde:

Ausnahmebedingungen sind nur in 'then' Blöcken erlaubt

    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

Dieser funktioniert nicht, da dieser Fehler von Spock beim Kompilieren ausgelöst wurde:

Ausnahmebedingungen sind nur als Anweisungen auf oberster Ebene zulässig

    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

Die empfohlene Lösung besteht darin, zwei Methoden zu verwenden: eine, die die guten Fälle testet, und eine, die die schlechten Fälle testet. Dann können beide Methoden Datentabellen verwenden.

Beispiel:

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

Sie können Ihren Methodenaufruf mit einer Methode umschließen, die die Nachricht oder die Ausnahmeklasse zurückgibt, oder eine Zuordnung von beiden. 

  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

Hier ist die Lösung, die ich mir ausgedacht habe. Es handelt sich im Wesentlichen um Variante 3, aber es wird ein try/catch-Block verwendet, um die Verwendung der Spock-Ausnahmebedingungen zu vermeiden (da diese müssen oberste Ebene sein müssen). 

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

Einige Vorbehalte:

  1. Sie benötigen mehrere catch-Blöcke, um verschiedene Ausnahmen zu testen.
  2. Sie müssen explizite Bedingungen (assert-Anweisungen) in try/catch-Blöcken verwenden.
  3. Sie können Ihren Stimulus und Ihre Antworten nicht in when-then-Blöcke aufteilen.
4
Ben Cass

An dem Beispiel von @AmanuelNega versuchte ich es auf der Spock-Webkonsole und speicherte den Code unter 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

So mache ich das, ich ändere die when:-Klausel, um immer eine Success -Ausnahme auszulösen, so dass Sie keine separaten Tests oder Logik benötigen, um zu sagen, ob thrown oder notThrown aufgerufen werden Rufen Sie einfach immer thrown mit der Datentabelle auf, ob Sie Success erwarten sollen oder nicht. 

Sie könnten Success umbenennen, um None oder NoException oder was auch immer Sie bevorzugen.

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

Eine weitere Sache, die ich ändern würde, wäre die Verwendung einer Unterklasse für die Fehlerausnahmen, um zu vermeiden, dass Success versehentlich erwischt wird, wenn Sie wirklich einen Fehler erwartet haben. Ihr Beispiel hat keine Auswirkungen, da Sie eine zusätzliche Prüfung für die Nachricht haben. Andere Tests testen jedoch möglicherweise nur den Ausnahmetyp.

class Failure extends Exception {}

und verwenden Sie diese oder eine andere "echte" Ausnahme anstelle der Vanille Exception

1
idij

Hier ein Beispiel, wie ich es mit @Unroll und den when:-, then:- und where:-Blöcken erreicht habe. Es läuft unter Verwendung aller 3 Tests mit den Daten aus der Datentabelle:

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