2

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.

mike rodent
  • 14,126
  • 11
  • 103
  • 157
  • Actually, thinking about it, I think that for the sake of testing speed (in unit tests at least) it is probably often desirable **not** to construct a new instance for each test. In particular, judicious use of the `cleanup` block at the end of each test, specifically resetting things which may have been unset, can make a dramatic difference to timings, compared to minting a new instance each time... – mike rodent Apr 07 '18 at 15:03

2 Answers2

9

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
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Just in case you have seen my original answer: I have updated it in order to also show how to use stubs, mocks and spies. – kriegaex Apr 02 '18 at 06:11
  • Thanks very much. I don't understand too well when you say "test that a singleton is not a singleton". My aim is either to test the functionality of a singleton, as the class-under-test, or to test the functionality of something else which has side effects on a singleton. I'm a low-intermediate TDD practitioner and I'd like to understand your thinking ... – mike rodent Apr 02 '18 at 07:22
  • Well, if you re-instantiate a singleton, you are effectively not treating it as a singleton anymore, i.e. not testing its real behaviour. I do understand that there might be cases in which you might need to test newly initialised singletons with different configurations, though. For that matter you could either use different Spock specs instead of putting everything into one spec. Or, as you suggested, you could just make sure that the singleton gets properly reconfigured before a feature method tests that configuration. You can use my trick, just use it responsibly and sparingly. – kriegaex Apr 02 '18 at 07:50
  • As you mentioned, `clazz.getDeclaredField("instance").set(clazz.instance, instance)` works only if `instance` is unset ... and unfortunately I'm getting currently unpredictable occasions when after `field.set(clazz.instance, null)` this `clazz.instance` is non-null. This can then lead to the field values of the old instance being (weirdly) given to the new one at line `clazz.getDeclaredField("instance").set(clazz.instance, instance)` Could this be thread-related in some way? Investigating... – mike rodent Apr 02 '18 at 19:12
  • I see your own deleted answer. Is this question somehow related to it? I think if you have a special case, then you should ask a new question linking to this one and really provide a full [MCVE](http://stackoverflow.com/help/mcve), so I can see what is going on. I cannot see what your `ConsoleHandler` does and how it is used during the test. – kriegaex Apr 03 '18 at 03:19
  • I have slightly updated my sample code, reorganising some details in the helper class and commenting out the "unset" call in the spy test, explaining why it is unnecessary for objects created by Objenesis. This is unrelated to your follow-up question, BTW. I just like the code better now. It still does the same as before. – kriegaex Apr 03 '18 at 04:00
  • Thanks again... I'm going to have to do some biggish experimenting to understand when and why `field.set( clazz.instance, null )` fails to set `instance` to null. I'm reverting to my `reset` workaround in the mean time. – mike rodent Apr 03 '18 at 20:20
  • Look, I have not written an extensive answer for you not to use it because you have some kind of problem with it. Just share some information, man! Where is the [MCVE](http://stackoverflow.com/help/mcve)? Maybe you don't need big experiments, global mocks and speculations but just a fresh pair of eyes. But please share more than just a tiny test snippet. Nobody can see what your test does, which class(es) it tests and how those are used. Probably the solution is simple. – kriegaex Apr 04 '18 at 01:15
  • I just saw that I had made a copy and paste mistake in my sample code. The singleton class `Highlander` was not there, instead I had `GroovySingletonTool` posted twice. This is fixed now. Sorry for noticing so late. – kriegaex Jul 03 '18 at 02:05
  • Hi, Thanks very much. I'll give it another try in due course (might take a few days). The problem I had, IIRC, is that your solution worked find in a single file (Specification), but some problem occurred in a "real world" situation with several Specifications. I am still therefore using a "reset method" solution in this project and it'd be great if the "instance" object could be recreated with each test. – mike rodent Jul 03 '18 at 10:24
  • The solution is still basically the same. There was a similar question today, that's why I returned here and noticed. If you have an MCVE of your failing situation on GitHub for me, I will take a look. Still being curious... – kriegaex Jul 03 '18 at 11:20
-1

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

mike rodent
  • 14,126
  • 11
  • 103
  • 157
  • Now I am really pissed. You un-accept my answer and accept your own without showing code proving what was wrong about mine and how to reproduce it. You give us only prose and accept that as an answer even though in your question you asked for solutions not involving manual reset. As for the solution being hacky and the whole contrived way of testing the singleton, I even warned you about it at the beginning of my question. You don't know what you want and make it really worth spending my time on you, thank you so much. I still think my solution works and you are just using it wrong. – kriegaex Apr 11 '18 at 14:34