SpockFramework 全局模拟未按预期工作

SpockFramework Global Mocks not working as expected

我使用以下测试作为示例来展示我遇到的类似问题。我认为这只是我对全局模拟在 SpockFramework 中的工作方式的误解。

  void "test"() {
    when:
    TestStage stage = new TestStage("John")
    GroovyMock(TestStep.class, global: true) {
      getName() >> "Joe"
    }
    then:
    stage.run() == "Joe"
  }

此测试应创建一个提供默认名称的测试阶段。但是后来我在 TestStage 中创建了一个 class 的全局模拟来覆盖 return 值。 IE:我只是想测试 TestStage 而不是 TestStep 的功能。如果 TestStep 只进行更改,我不想了解它们,我将单独测试它们。然而,当我 运行 这个测试时,看起来全局模拟从未生效,因为 returned 名称仍然是 "John",这是我最初提供的。

stage.run() == "Joe"
|     |     |
|     John  false

这是用于测试的两个示例 classes。

class TestStage {
  TestStep step

  TestStage(String name) {
    this.step = new TestStep(name)
  }

  String run() {
    return step.getName()
  }

}
class TestStep {
    private String name

    TestStep(String name) {
      this.name = name
    }

    String getName() {
      return this.name
    }
}

其实,你问的这个问题很好。根据 Spock 手册,似乎您可以使用 GroovyMockGroovyStub 来全局替换实例并按照您尝试的方式存根它们的方法,即使我是您我也会创建在依赖它的对象的构造函数中隐式使用它之前,首先使用全局模拟对象。但是无论如何,它并没有像你说的那样工作。

当我在 Spock 手册和 Spock 源代码中搜索有关 GroovyMock 的示例时,我在任何地方都找不到涉及静态方法的任何示例。实际上,那里的测试覆盖率很差。通常,如果手册对我没有帮助,我会看看是否可以从测试中推断出如何使用某个功能。这种情况,只好自己试了。

我注意到的第一件事是完全违反直觉的事实,即 在全局 GroovyMockGroovyStub 上调用构造函数时,它 returns null!!! 这是一个真正的警告。在某种程度上,构造函数在这里被视为普通的模拟方法,也 returning null。任何官方来源都没有提到这一点,我也认为应该将其更改为默认 returning 普通的 Spock 模拟(如果 class 是可模拟的,即非最终的)。

现在这也是 解决方案的关键:您需要将一个或多个构造函数存根到 return 除了 null 之外的其他东西,例如先前创建的普通实例或 Spock mock/stub/spy.

这是您的源代码的一个略微改动的版本。我将应用程序重命名为 classes,以便在它们的名称中不包含 Test。所有这些 Test class 名称让我有点困惑,特别是因为我也将我的 Spock 规范命名为 *Test,而不是 *Spec,因为 Maven Surefire/Failsafe无需额外配置即可自动检测

我还向要模拟的 class 添加了一个静态方法,以便向您展示存根的语法。那只是一个免费的附加组件,与您的问题没有直接关系。

我的测试显示三个变体:

  • 使用 classical Spock mock 并将其注入被测对象
  • 使用全局 GroovySpy,它始终基于真实对象(或指示创建一个对象)。因此,您不需要存根构造器。
  • 使用带有显式存根构造函数的全局 GroovyMock。在我的示例中,它 return 是一个带有存根方法的常规 Spock 模拟,但它也可以 return 一个普通实例。
package de.scrum_master.Whosebug.q61667088

class Step {
  private String name

  Step(String name) {
    this.name = name
  }

  String getName() {
    return this.name
  }

  static String staticMethod() {
    return "original"
  }
}
package de.scrum_master.Whosebug.q61667088

class Stage {
  Step step

  Stage(String name) {
    this.step = new Step(name)
  }

  String run() {
    return step.getName()
  }
}
package de.scrum_master.Whosebug.q61667088

import spock.lang.Specification

class GlobalMockTest extends Specification {

  def "use Spock mock"() {
    given:
    def step = Mock(Step) {
      getName() >> "Joe"
    }
    def stage = new Stage("John")
    stage.step = step

    expect:
    stage.run() == "Joe"
  }

  def "use global GroovySpy"() {
    given:
    GroovySpy(Step, global: true) {
      getName() >> "Joe"
    }
    Step.staticMethod() >> "stubbed"
    def stage = new Stage("John")

    expect:
    Step.staticMethod() == "stubbed"
    stage.run() == "Joe"
  }

  def "use global GroovyMock"() {
    given:
    GroovyMock(Step, global: true)
    new Step(*_) >> Mock(Step) {
      getName() >> "Joe"
    }
    Step.staticMethod() >> "stubbed"
    def stage = new Stage("John")

    expect:
    Step.staticMethod() == "stubbed"
    stage.run() == "Joe"
  }

}

P.S.: 我认为,您可能阅读了 Spock 手册,但以防万一:如果您的 GroovyMock/Stub/Spy 目标是使用 Groovy 以外的语言实现的,例如 Java 或 Kotlin,这将不起作用,因为 Groovy* 将表现得像普通的 Spock mock/stub/spy.


更新: 我刚刚创建了 Spock issue #1159,首先是为了让测试记录并涵盖此行为,但为了让它也发生变化,如果不是这样的话。