Spock - 使用数据表测试异常

73

如何用Spock以一种良好的方式(例如数据表)测试异常?

示例:有一个方法validateUser,如果用户有效,它可能会抛出不同消息或不抛出异常。

规范类本身:

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

变体1

这个方案是可行的,但是由于所有的when/then标签和反复调用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'
    }

变体 2

这个不起作用,是因为Spock在编译时引发了这个错误:

例外情况仅在“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' }
    }

变量3

这个不起作用是因为Spock在编译时抛出了以下错误:

异常条件只允许作为顶级语句

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

上周遇到了同样的情况,我正是按照 @peter 建议的做法去处理的。:) 根据一个数据表来处理两种异常情况(抛出/未抛出)并不可取。你甚至不能在数据表中包含已抛出的异常。 - dmahapatro
7个回答

64

推荐的解决方案是有两种方法:一种测试正常情况,另一种测试异常情况。然后这两种方法都可以使用数据表。

例如:

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

}

2
我记得有一个问题,如果没有抛出MyException,那么thrown(MyException)能返回null吗? - dmahapatro
我需要重新检查我的测试。但是在数据表中使用thrown()/notThrown()时出现了错误。无论如何,感谢您提供了一个精彩的测试框架。因为您,我成为了工作中的“BDD开发者”。;) - dmahapatro
我可能可以用那种方式工作,但目前您无法将null传递给thrown() - Peter Niederwieser
3
@PeterNiederwieser,如果您能提供如何在数据表中处理异常的基本示例,那将非常有用。在Google上搜索“spock data exceptions”的结果中,这是排名最高的,并且提供一个参考示例(或指向文档)将非常有帮助。谢谢。 - Will

9

这是我想出来的解决方案。它基本上是变体3,但它使用try/catch块来避免使用Spock的异常条件(因为那些必须是顶级)。

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

注意事项:

  1. 您需要使用多个catch块测试不同的异常。
  2. 您必须在try/catch块内使用显式条件(assert语句)。
  3. 您不能将刺激和响应分离成when-then块。

2
完美地解决了我的问题。我刚刚更新了代码,只有在提供了消息的情况下才检查异常:assert!exceptionMessage,并且可以删除 expectException 列。 - Scott Langeberg

6
您可以将方法调用包装在一个返回消息或异常类,或两者的映射的方法中...
  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
这是我的做法,我修改了when:子句,使其总是抛出一个Success异常,这样你就不需要单独的测试或逻辑来告诉是否调用thrownnotThrown,只需始终使用数据表调用thrown,并告知是否期望Success。你可以将Success重命名为NoneNoException或其他你喜欢的名称。
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')
    }
}

我建议对失败异常也使用子类,以避免在实际期望失败时意外捕获Success。这不会影响你的示例,因为你还有一个额外的消息检查,但其他测试可能只测试异常类型。

class Failure extends Exception {}

使用其他“真实”异常代替基本的Exception异常。


在我看来,抛出一个表示成功的异常是很不好的编程习惯。请参考Effective Java第69条款:仅将异常用于异常情况。 - entpnerd
3
只有在测试中才会出现这种情况,以解决框架的一个限制,因此,在我看来,这种启发式方法不适用,或者被它掩盖的其他问题更加突出。 - idij

5
我有一个解决方案,不会干扰你的测试工作流程,而且你可以通过动态对象在表中放置的内容来分析异常。
@Unroll
def "test example [a=#a, b=#b]"() {
    given:
    def response
    def caughtEx

    when:
    try {
      result = someAmazingFunctionWhichThrowsSometimes(a,b)
    } catch (Exception ex) {
      caughtEx = ex
    }

    then:
    result == expected

    if (exception.expected) {
        assert caughtEx != null && exception.type.isInstance(caughtEx)
    } else {
        assert caughtEx == null
    }

    where:
    a    | b    || exception                                  | expected
    8    | 4    || [expected: false]                          | 2
    6    | 3    || [expected: false]                          | 3
    6    | 2    || [expected: false]                          | 3
    4    | 0    || [expected: true, type: RuntimeException]   | null

}

3

借鉴@AmanuelNega的示例,我在Spock Web控制台上尝试了一下,并将代码保存在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
        }
    }
}
​

0
这是一个使用@Unrollwhen:then:where:块实现的示例。它可以使用数据表中的所有3个测试运行:
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' | _
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接