Groovy Singleton and testing issue (with Spock)

倖福魔咒の 提交于 2019-12-23 22:10:23

问题


There's a discusson here about testing and singletons... but that is about Java patterns.

My question is specifically about the Groovy @Singleton (annotation) way of implementing this pattern.

This seems like another bit of Groovy Goodness. But I have a bit of a problem when testing (with Spock) using a class which has this annotation.

If any of the state of this instance changes during a test (from the pristine, just-constructed state), as far as my experiments indicate this will then carry through to the next test... I tested MySingletonClass.instance's hashCode() with several tests and they all came back the same. Perhaps this isn't surprising.

But ... wouldn't it be better if Spock could (using the kind of uber-Groovy magic I can only speculate on) somehow reset the class between tests? I.e. by creating a new instance?

There is an obvious workaround: to incorporate a reset method into each @Singleton class where its state might change during a test. And then call that reset method in setup() ... in fact I use a common Specification subclass, CommonProjectSpec, from which all my real Specifications subclass... so that would be simple enough to implement.

But it seems a bit inelegant. Is there any other option? Should I maybe submit this as a Spock suggested enhancement?

PS it also turns out you can't then make a Spy of this class (or a GroovySpy). But you can make a Mock of it:

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

... yes, the "global" GroovyMock here actually has the ability to "tame" the static instance field so that it meekly accepts a Mock cuckoo in the nest.


回答1:


So basically you want to test that a singleton is not a singleton. This strikes me as rather odd. But anyway, I am regarding this question rather as a puzzle which I am going to solve for its own sake because it is a nice challenge. (Don't do this at home, kids!)

Groovy singleton:

package de.scrum_master.stackoverflow

@Singleton
class Highlander {
  def count = 0

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

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

Singleton helper class:

package de.scrum_master.stackoverflow

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

Spock test:

This test shows how to

  • re-instantiate a Groovy singleton before feature method execution
  • use a Stub() for a Groovy singleton
  • use a Mock() for a Groovy singleton
  • use a Spy() for a Groovy singleton (needs Objenesis)
package de.scrum_master.stackoverflow

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

Console log:

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



回答2:


Unfortunately I ran into big problems with Kriegax's otherwise helpful solution.

I've done quite a bit of experimentation and have been unable to explain where the problem comes from. Although there is a possible clue here. (Incidentally I did try this idea of applying the change of modifier immediately after setting the new instance to be the singleton instance... it did not solve the problem).

In a typical situation I find I may have a Specification with maybe 15 features (tests). Running these on their own works fine: the MySingleton.instance field is set first to null and then to a new instance of MySingleton.

But then when I try to run this with another xxx.groovy file with another Specification, it will work OK for about 8 features ... but then I add a new feature (i.e. I'm basically uncommenting existing features as I go) suddenly the problem crops up: MySingleton.instance can be set to null... but refuses point blank to be set to a new instance. I even tried a for loop with a Thread.sleep() to see if trying multiple times might solve it.

Naturally I then had a look at the offending feature which had just been added: but there was nothing there that I hadn't done in other features. Worse, far worse, follows: I then find that these results are not consistent: sometimes the "offending" new feature, once uncommented, does NOT then trigger the failure of Field.set( ... ) in the other .groovy file. By the way, no Exception is thrown by Field.set( ... ) during any of this.

It should be noted, en passant, that field.modifiers &= ~Modifier.FINAL is said to be "a hack", as described here, for example, with many caveats about its use.

I've therefore reluctantly come to the conclusion that if you want to have one or more singleton classes with Groovy you either have to have a reset method, which can be guaranteed to return the instance to a pristine (newly constructed) state, or you have to abandon use of the @Singleton annotation (i.e. if you are keen to construct a new instance with each feature).



来源:https://stackoverflow.com/questions/49602657/groovy-singleton-and-testing-issue-with-spock

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!