Exception Spock-使用数据表测试异常

Exception Spock-使用数据表测试异常,exception,testing,groovy,spock,Exception,Testing,Groovy,Spock,如何使用Spock以良好的方式测试异常(例如数据表) 示例:具有一个方法validateUser,该方法可以使用不同的消息引发异常,或者如果用户有效,则不会引发异常 规范类本身: class User { String userName } class SomeSpec extends spock.lang.Specification { ...tests go here... private validateUser(User user) { if (!u

如何使用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)
的重复调用弄得一团糟

变体2

由于Spock在编译时引发的错误,此错误无法工作:

异常条件仅在“then”块中允许

变体3

由于Spock在编译时引发的错误,此错误无法工作:

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


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

例如:

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

}
class SomeSpec扩展了规范{
类用户{String userName}
def“验证有效用户”(){
什么时候:
验证用户(用户)
然后:
noException抛出()
哪里:

用户您可以使用返回消息或异常类的方法或两者的映射来包装方法调用

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

使用@AmanuelNega中的示例,我在spock web控制台上尝试了一下,并将代码保存在


下面是我如何使用
@Unroll
when:
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' | _
    }
}

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

一些注意事项:

  • 您需要多个catch块来测试不同的异常
  • 您必须在try/catch块内使用显式条件(
    assert
    语句)
  • 你不能将你的刺激和反应分成
    块,然后

  • 我是这样做的,我修改了
    when:
    子句,使其始终抛出
    成功
    异常,这样您就不需要单独的测试或逻辑来判断是否调用
    抛出
    不抛出
    ,只需始终调用
    抛出
    ,数据表告诉您是否期望
    成功

    您可以将
    Success
    重命名为
    None
    NoException
    或任何您喜欢的名称

    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')
        }
    }
    
    另外一件我要更改的事情是,也为失败异常使用一个子类,以避免在您真正期望失败时意外捕获
    成功
    。这不会影响您的示例,因为您有一个额外的消息检查,但其他测试可能只测试异常类型

    class Failure extends Exception {}
    

    并使用该异常或其他“真实”异常,而不是普通的异常
    异常

    我有一个不会扭曲测试工作流的解决方案,您可以通过放置在where表中的动态对象的内容来分析异常

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

    上周遇到了相同的情况,我完全按照@peter的建议做了。:)基于一个数据表处理两个异常值(抛出/未抛出)不是办法。你甚至不能在数据表中抛出异常。我记得有一个问题,不能
    抛出(MyException)
    如果未抛出MyException
    则返回null?我将不得不重新访问我的测试。但是我在数据表中使用了shown()/notshown()时出错。无论如何,感谢您提供了一个出色的测试框架。我成为了“那个BDD开发人员”在我的工作中,因为你。;)我可能会这样做,但目前你不能将
    null
    传递给
    shown()
    @PeterNiederwieser这对于如何处理数据表中的异常非常有用。这是Google上“spock数据异常”的热门示例,也是一个参考示例(或指向文档)非常有帮助。非常适合我的情况。我刚刚更新,仅当提供消息时才检查异常:
    assert!exceptionMessage
    ,并且可以删除
    expectException
    列。IMO,抛出表示成功的异常是非常糟糕的代码味道。请参阅有效的Java项目69:仅对异常使用异常只有在测试中,它才会被抛出,绕过框架的限制,所以我认为这种启发式不适用,或者被它掩盖的其他气味所掩盖。
    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' | _
        }
    }
    
    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'
    }
    
    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')
        }
    }
    
    class Failure extends Exception {}
    
    @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
    
    }