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.

1
  • 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... Commented Apr 7, 2018 at 15:03

2 Answers 2

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 
Sign up to request clarification or add additional context in comments.

11 Comments

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.
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 ...
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.
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...
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, so I can see what is going on. I cannot see what your ConsoleHandler does and how it is used during the test.
|
-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).

1 Comment

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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.