Groovy 单例和测试问题(与 Spock)

Groovy Singleton and testing issue (with Spock)

有一个关于测试和单例的讨论 here...但那是关于 Java 模式的。

我的问题具体是关于实现此模式的 Groovy @Singleton(注释)方式。

这似乎又是一个 Groovy 好东西。但是我在使用具有此注释的 class 进行测试(使用 Spock)时遇到了一些问题。

如果此实例的任何状态在测试期间发生变化(从原始的、刚刚构建的状态),据我的实验表明,这将继续进行下一个测试...我测试了 MySingletonClass.instancehashCode() 进行了几次测试,结果都一样。或许这并不奇怪。

但是...如果 Spock 可以(使用我只能推测的那种 uber-Groovy 魔法)以某种方式在测试之间重置 class 不是更好吗? IE。通过创建新实例?

有一个明显的解决方法:将 reset 方法合并到每个 @Singleton class 中,其状态可能在测试期间发生变化。然后在 setup() 中调用那个 reset 方法...事实上我使用一个普通的 Specification subclass、CommonProjectSpec,我所有的真实 Specifications subclass... 这样实现起来就足够简单了。

不过好像有点不雅。还有其他选择吗?我是否应该将其作为 Spock 建议的增强功能提交?

PS 事实证明,您无法再对 class(或 GroovySpy)进行 Spy。但是你可以制作一个Mock

    ConsoleHandler mockCH = Mock( ConsoleHandler ){
        getDriver() >> ltfm
    }
    GroovyMock( ConsoleHandler, global: true )
    ConsoleHandler.instance = mockCH

... 是的,这里的 "global" GroovyMock 实际上有能力 "tame" 静态 instance 字段,以便它温顺地接受 Mock杜鹃在巢

所以基本上你想测试一个单例是不是单例。这让我觉得很奇怪。但无论如何,我将这个问题视为一个难题,我将为其本身解决这个难题,因为这是一个很好的挑战。 (孩子们不要在家里这样做!)

Groovy单例:

package de.scrum_master.Whosebug

@Singleton
class Highlander {
  def count = 0

  def fight() {
    println "There can be only one!"
    count++
    doSomething()
  }

  def doSomething() {
    println "Doing something"
  }
}

单例助手class:

package de.scrum_master.Whosebug

import java.lang.reflect.Field
import java.lang.reflect.Modifier

class GroovySingletonTool<T> {
  private Class<T> clazz

  GroovySingletonTool(Class<T> clazz) {
    this.clazz = clazz
  }

  void setSingleton(T instance) {
    // Make 'instance' field non-final
    Field field = clazz.getDeclaredField("instance")
    field.modifiers &= ~Modifier.FINAL
    // Only works if singleton instance was unset before
    field.set(clazz.instance, instance)
  }

  void unsetSingleton() {
    setSingleton(null)
  }

  void reinitialiseSingleton() {
    // Unset singleton instance, otherwise subsequent constructor call will fail
    unsetSingleton()
    setSingleton(clazz.newInstance())
  }
}

斯波克测试:

这个测试展示了如何

  • re-instantiate一个Groovy特征方法执行前的单例
  • 对Groovy单例使用Stub()
  • 对Groovy单例使用Mock()
  • 使用 Spy() 作为 Groovy 单例(需要 Objenesis)
package de.scrum_master.Whosebug

import org.junit.Rule
import org.junit.rules.TestName
import spock.lang.Specification
import spock.lang.Unroll

class HighlanderTest extends Specification {
  def singletonTool = new GroovySingletonTool<Highlander>(Highlander)
  @Rule
  TestName gebReportingSpecTestName

  def setup() {
    println "\n--- $gebReportingSpecTestName.methodName ---"
  }

  @Unroll
  def "Highlander fight no. #fightNo"() {
    given:
    singletonTool.reinitialiseSingleton()
    def highlander = Highlander.instance

    when:
    highlander.fight()

    then:
    highlander.count == 1

    where:
    fightNo << [1, 2, 3]
  }

  @Unroll
  def "Highlander stub fight no. #fightNo"() {
    given:
    Highlander highlanderStub = Stub() {
      fight() >> { println "I am a stub" }
    }
    singletonTool.setSingleton(highlanderStub)
    def highlander = Highlander.instance

    when:
    highlander.fight()

    then:
    highlander == highlanderStub

    where:
    fightNo << [1, 2, 3]
  }

  @Unroll
  def "Highlander mock fight no. #fightNo"() {
    given:
    Highlander highlanderMock = Mock() {
      fight() >> { println "I am just mocking you" }
    }
    singletonTool.setSingleton(highlanderMock)
    def highlander = Highlander.instance

    when:
    highlander.fight()

    then:
    highlander == highlanderMock
    0 * highlander.doSomething()

    where:
    fightNo << [1, 2, 3]
  }

  @Unroll
  def "Highlander spy fight no. #fightNo"() {
    given:
    // Unset not necessary because Objenesis creates object without constructor call
    // singletonTool.unsetSingleton()
    Highlander highlanderSpy = Spy(useObjenesis: true)
    // Spy's member is not initialised by Objenesis
    highlanderSpy.count = 0
    singletonTool.setSingleton(highlanderSpy)
    def highlander = Highlander.instance

    when:
    highlander.fight()

    then:
    highlander == highlanderSpy
    highlander.count == 1
    1 * highlander.doSomething() >> { println "I spy" }

    where:
    fightNo << [1, 2, 3]
  }
}

控制台日志:

--- Highlander fight no. 1 ---
There can be only one!
Doing something

--- Highlander fight no. 2 ---
There can be only one!
Doing something

--- Highlander fight no. 3 ---
There can be only one!
Doing something

--- Highlander stub fight no. 1 ---
I am a stub

--- Highlander stub fight no. 2 ---
I am a stub

--- Highlander stub fight no. 3 ---
I am a stub

--- Highlander mock fight no. 1 ---
I am just mocking you

--- Highlander mock fight no. 2 ---
I am just mocking you

--- Highlander mock fight no. 3 ---
I am just mocking you

--- Highlander spy fight no. 1 ---
There can be only one!
I spy

--- Highlander spy fight no. 2 ---
There can be only one!
I spy

--- Highlander spy fight no. 3 ---
There can be only one!
I spy

不幸的是,我 运行 遇到了 Kriegax 的其他有用解决方案的大问题。

我做了很多实验,但无法解释问题出在哪里。虽然有可能的线索。 (顺便说一句,我确实尝试过将新实例设置为单例实例后立即应用修饰符更改的想法......它没有解决问题)。

在典型情况下,我发现我可能有一个 Specification,可能有 15 个特征(测试)。 运行 它们自己工作正常:MySingleton.instance 字段首先设置为 null,然后设置为 MySingleton.

的新实例

但是当我尝试 运行 将另一个 xxx.groovy 文件与另一个 Specification 文件一起使用时,它可以正常工作大约 8 个功能......但随后我添加了一个新的功能(即我基本上是在我去的时候取消注释现有功能)突然出现问题:MySingleton.instance 可以设置为 null... 但拒绝将空白设置为新实例。我什至尝试了一个带有 Thread.sleep()for 循环,看看尝试多次是否可以解决问题。

然后我自然而然地查看了刚刚添加的有问题的功能:但在其他功能中我没有做任何事情。更糟的是,更糟的是:然后我发现这些结果不一致:有时 "offending" 新功能一旦取消注释,就不会触发另一个 .[=44= 中 Field.set( ... ) 的失败] 文件。顺便说一句,在此期间 Field.set( ... ) 不会抛出 Exception

顺便说一下,field.modifiers &= ~Modifier.FINAL 据说是 "a hack",如 here 所述,例如,关于它的使用有很多注意事项。

因此我不情愿地得出结论,如果你想有一个或多个单例 类 和 Groovy 你要么必须有一个 reset 方法,它可以gua运行teed return 实例到原始(新建)状态,或者你必须放弃使用 @Singleton 注释(即,如果你热衷于构造一个新实例每个功能)。