Spock 在Where 块中测试异常处理

问题描述 投票:0回答:2

我正在测试一个有一些依赖项的服务方法;我想断言,如果这些依赖项中的任何一个抛出异常,则服务方法应该返回默认值。

我想编写的服务和测试看起来像这样。

static class Service {
    def dependency1
    def dependency2
    def dependency3

    def method() {
        try {
            def foo = dependency1.get()
            def bar = dependency2.get()
            def baz = dependency3.get()
            return " $foo $bar $baz "
        } catch (Exception e) {
            println e
            return ' default value '
        }
    }
}

def 'test Service error handling'() {
    given:
    def dependency1 = Mock(Supplier)
    def dependency2 = Mock(Supplier)
    def dependency3 = Mock(Supplier)
    def serviceUnderTest = new Service(dependency1: dependency1, dependency2: dependency2, dependency3: dependency3)

    when:
    def result = serviceUnderTest.method()

    then:
    result == ' default value '
    dependency1.get() >> closure1
    dependency2.get() >> closure2
    dependency3.get() >> closure3

    where:
    closure1                              | closure2                              | closure3
    {-> throw new Exception('closure1') } | {-> null }                            | {-> null };
    {-> null}                             | {-> throw new Exception('closure2') } | {-> null };
    {-> null}                             | {-> null}                             | {-> throw new Exception('closure3') }
}

此测试不起作用,因为它会导致模拟返回文字闭包而不是这些闭包的结果。当然,这是由于添加了

where
块造成的,因为任何模拟都可以直接返回单个闭包的结果,即
dependency1.get() >> { throw new Exception() }

我是否被迫将其编写为三个单独的测试,或者是否有其他方法可以将它们结合起来?

exception groovy spock data-driven-tests
2个回答
3
投票

如果你写

dependency1.get() >> closure1

您的模拟将返回闭包本身而不对其进行评估。评估仅发生在 GroovyString

" $foo $bar $baz "
内部,将评估期间发生的错误消息扩展到其中,但不会升级该异常。

你想用

dependency1.get() >> { closure1() }

为了修复您的测试。

()
评估您的闭包,但同时周围的闭包
{}
确保仅在调用存根方法时才进行评估,而不是在定义它时进行评估。

一些改进想法:

  • 如何展开您的测试,将其分成多个具有参数化名称的方法?这还有一个很好的副作用,可以帮助 IDE 和 Groovy 编译器解析您的
    where:
    块,而无需分号和
    { -> ...
    语法。
  • 如何在
    given:
    块中而不是在它们不属于的
    then:
    块中存根模拟方法?
  • 如何在模拟定义中存根方法以使测试更加紧凑?
  • 如何按照 Spock 手册的建议,将这个简单情况下的
    when: ... then:
    替换为
    expect:
package de.scrum_master.stackoverflow.q57172322

import spock.lang.Specification
import spock.lang.Unroll

class ServiceDependenciesThrowingErrorsTest extends Specification {

  @Unroll
  def 'handle error in service #serviceName'() {
    given:
    def serviceUnderTest = new Service(
      dependency1: Mock(Supplier) { get() >> { closure1() } },
      dependency2: Mock(Supplier) { get() >> { closure2() } },
      dependency3: Mock(Supplier) { get() >> { closure3() } }
    )

    expect:
    serviceUnderTest.method() == 'default value'

    where:
    serviceName | closure1                            | closure2                            | closure3
    "A"         | { throw new Exception('closure1') } | { null }                            | { null }
    "B"         | { null }                            | { throw new Exception('closure2') } | { null }
    "C"         | { null }                            | { null }                            | { throw new Exception('closure3') }
  }

  static class Service {
    def dependency1
    def dependency2
    def dependency3

    def method() {
      try {
        def foo = dependency1.get()
        def bar = dependency2.get()
        def baz = dependency3.get()
        return "$foo $bar $baz"
      } catch (Exception e) {
        println e
        return 'default value'
      }
    }
  }

  static class Supplier {
    def get() {
      "OK"
    }
  }

}

这是展开时测试执行在我的 IDE (IntelliJ IDEA) 中的样子:

Unrolled test execution in IntelliJ IDEA


0
投票

参考 @kriegaex 的答案,您可以通过将实际的异常抛出移至辅助方法来使其变得更加紧凑。此外,如果您需要检查异常冒泡/包装,外部字段也可以提供帮助。

这是一个更新的示例:

package de.scrum_master.stackoverflow.q57172322

import spock.lang.Specification
import spock.lang.Unroll

class ServiceDependenciesThrowingErrorsTest extends Specification {
  
  def e(String name) {
    throw new RuntimeException(name)
  }

  @Unroll
  def 'handle error in service #serviceName'() {
    given:
    def serviceUnderTest = new Service(
      dependency1: Mock(Supplier) { get() >> { a() } },
      dependency2: Mock(Supplier) { get() >> { b() } },
      dependency3: Mock(Supplier) { get() >> { c() } }
    )

    when:
    serviceUnderTest.method()

    then:
    Exception ex = thrown()
    ex.cause.message == name

    where:
    name | a           | b           | c
    "A"  | { e(name) } | {}          | {}
    "B"  | {}          | { e(name) } | {}
    "C"  | {}          | {}          | { e(name) }
  }

  static class Service {
    def dependency1
    def dependency2
    def dependency3

    def method() {
      try {
        def foo = dependency1.get()
        def bar = dependency2.get()
        def baz = dependency3.get()
        return "$foo $bar $baz"
      } catch (Exception e) {
        println e
        throw e
      }
    }
  }

  static class Supplier {
    def get() {
      "OK"
    }
  }

}

这使得这种方法可以扩展到更多的依赖项,同时保持表格式。

但是,如果您想要以这种方式测试“许多”依赖项,那么数据管道可能会更好地为您服务,因为它们可以让您使用更少的代码构建有效的稀疏矩阵。以下是一些有用的示例片段: // Build the closure matrix using list concatenations closure1 << [{}] * 00 + [{ e() }] + [{}] * 04 closure2 << [{}] * 01 + [{ e() }] + [{}] * 03 // Or automate that a bit closure1 << sparseExceptions(5, 0) closure2 << sparseExceptions(5, 1) def sparseExceptions(int count, int position) { [{}] * position + [{ e() }] + [{}] * (count - 1 - position) }

最后,稀疏矩阵的想法可以通过多变量数据管道发挥到极致。这允许在不牺牲名称的情况下进行显着扩展,但代价是失去对非异常存根值的控制:

[ name, closure1, closure2, closure3 ] << sparseMultiException([ "A", "B", "C" ]) def sparseExceptions(List<String> names) { names.indexed().collect { y, name -> [name] + (0..<names.size()).collect { x -> sparseExceptions(names.size(), x)[y] } } }

	
© www.soinside.com 2019 - 2024. All rights reserved.