【问题标题】:Spock - Testing Exceptions with Data TablesSpock - 使用数据表测试异常
【发布时间】:2013-10-11 17:02:27
【问题描述】:

如何使用 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 的建议做了。 :) 基于一个数据表处理两种异常变体(抛出/未抛出)不是方法。你甚至不能在数据表中抛出异常。

标签: exception testing groovy spock


【解决方案1】:

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

例子:

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

}

【讨论】:

  • 我记得一个问题,如果MyException没有被抛出,thrown(MyException)不能返回null吗?
  • 我将不得不重新审视我的测试。但是我在数据表中使用 throw()/notThrown() 时遇到了错误。无论如何感谢一个精彩的测试框架。因为你,我在工作中成为了“that-BDD-developer”。 ;)
  • 我可能会以这种方式工作,但目前您不能将null 传递给thrown()
  • @PeterNiederwieser 这对于如何处理数据表中的异常的基本示例非常有用。这是 Google 上“spock 数据异常”的热门话题,参考示例(或指向 doc)会非常有帮助,谢谢。
【解决方案2】:

这是我想出的解决方案。它基本上是变体 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 块。

【讨论】:

  • 非常适合我的情况。我刚刚更新为仅在提供消息时检查异常:assert !exceptionMessage,并且可以删除expectException 列。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

    我是这样做的,我将when: 子句修改为始终抛出Success 异常,这样您就不需要单独的测试或逻辑来判断是调用thrown 还是notThrown,只需始终使用数据表调用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

    【讨论】:

    • IMO,抛出表示成功的异常是一种非常糟糕的代码气味。请参阅有效的 Java 第 69 条:仅在异常情况下使用异常。
    • 它只是在测试中被抛出,以解决框架的限制,因此 IMO 这种类型的启发式方法不适用,或者被它掩盖的其他气味所压倒。跨度>
    【解决方案5】:

    使用来自 @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
            }
        }
    }
    ​
    

    【讨论】:

      【解决方案6】:

      我有不会扭曲您的测试工作流程的解决方案,您可以通过放置在 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
      
      }
      

      【讨论】:

        【解决方案7】:

        这是我如何使用@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' | _
            }
        }
        

        【讨论】:

          猜你喜欢
          • 2014-05-10
          • 2014-11-05
          • 1970-01-01
          • 1970-01-01
          • 2018-04-08
          • 2016-05-06
          • 2013-01-21
          • 2018-02-22
          • 1970-01-01
          相关资源
          最近更新 更多