0

I write a singleTone named DemoSingleTon, a main class named DemoMain and a test class named DemoTest. when testing all tests of DemoTest individually, tests run successfully.If all tests run together, the latter two use cases will always fail.It looks like the mockStatic behind doesn't take effect.

enter image description here

public final class DemoSingleTon {
    private static final DemoSingleTon instance = new DemoSingleTon();

    private DemoSingleTon() {
    }

    public static DemoSingleTon getInstance() {
        return instance;
    }

    public String test(String input) {
        return input == null ? "" : input;
    }
}
public class DemoMain {

    private static final DemoSingleTon instance = DemoSingleTon.getInstance();

    public static String testInput() {
        return TestUtil.test("");
    }

    public String testInputUseSingleTone() {
        return instance.test("input1");
    }
}
public class DemoTest {

    @Test
    public void test() {
        try (MockedStatic<DemoSingleTon> mockedStatic = Mockito.mockStatic(DemoSingleTon.class)) {
            DemoSingleTon testUtil1 = Mockito.mock(DemoSingleTon.class);
            mockedStatic.when(DemoSingleTon::getInstance).thenReturn(testUtil1);
            Mockito.when(testUtil1.test("input1")).thenReturn("nothing");
            DemoMain demoMain = new DemoMain();
            assertEquals("nothing", demoMain.testInputUseSingleTone());
        }
    }

    @Test
    public void test1() {
        try (MockedStatic<DemoSingleTon> mockedStatic = Mockito.mockStatic(DemoSingleTon.class)) {
            DemoSingleTon testUtil1 = Mockito.mock(DemoSingleTon.class);
            mockedStatic.when(DemoSingleTon::getInstance).thenReturn(testUtil1);
            Mockito.when(testUtil1.test("input1")).thenReturn("everything");
            DemoMain demoMain = new DemoMain();
            assertEquals("everything", demoMain.testInputUseSingleTone());
        }
    }

    @Test
    public void test2() {
        DemoMain demoMain = new DemoMain();
        assertEquals("input1", demoMain.testInputUseSingleTone());

    }
}

build.gradle following:

testImplementation group: 'org.mockito', name: 'mockito-inline', version: '4.9.0'
testImplementation('org.junit.jupiter:junit-jupiter-api:5.6.2')
testImplementation('org.junit.jupiter:junit-jupiter-engine:5.6.2')

I think each call to mockitoStatic should be independent and not interact with each other.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
ppanda2021
  • 15
  • 6
  • Please learn some more Stack Overflow markdown syntax. Like in your previous questions, your inline image was not visible but just a link, and I fixed it. I also made sure that yor code snippets have syntax highlighting. I shall look into the question later, if nobody else is quicker. – kriegaex Dec 06 '22 at 09:27
  • Thanks for the reminder. I'll pay attention next time. Still expect you to be able to reply the above questions – ppanda2021 Dec 06 '22 at 10:18

3 Answers3

0
public class DemoMain {
    private static final DemoSingleTon instance = DemoSingleTon.getInstance();
}

executes DemoSingleton.getInstance() only once the first time the class is loaded and assigns the reference to the instance field. Once that has happened, your static mock cannot intercept the method call anymore, because the method is never called again. Loading the class happens on first access to the class, meaning:

        try (MockedStatic<DemoSingleTon> mockedStatic = Mockito.mockStatic(DemoSingleTon.class)) {
            DemoSingleTon testUtil1 = Mockito.mock(DemoSingleTon.class);
            mockedStatic.when(DemoSingleTon::getInstance).thenReturn(testUtil1);
            Mockito.when(testUtil1.test("input1")).thenReturn("nothing");

            DemoMain demoMain = new DemoMain(); // <-- class loaded here and DemoSingleton.getInstance is called (which is intercepted by your mock)

            assertEquals("nothing", demoMain.testInputUseSingleTone());
        }

The behavior would be different if you changed your main class implementation to:

public class DemoMain {
    public String testInputUseSingleTone() {
        return DemoSingleTon.getInstance().test("input1");
    }
}

Because now, DemoSingleton.getInstance() is called every time you call your method and thus can be intercepted by the static mock.

In essence, this question is another variation of Why is my class not calling my mocked methods in unit test?, albeit a little trickier. Do yourself a favor and take a step back to think about how you can make your design more testable, without requiring static mocks.

knittl
  • 246,190
  • 53
  • 318
  • 364
  • Thank you very much for your answer, I followed your method to test.All tests can pass. – ppanda2021 Dec 07 '22 at 02:12
  • On the one hand, the original design of some code was static or singleton implementation, and it's hard to refactor.On the other hand, we want to reduce the number of object creations to meet the minimum memory requirements, so we refer to org.springframework.util. All in all, thank you very much for your help @knittl . I will make my design more testable as much as possible. – ppanda2021 Dec 07 '22 at 02:26
  • Panda, If you make your singleton a little less secure and e.g. change `DemoSingleTon.instance` to be non-final and protected instead of private, a test which is in the same package could easily inject a mock, and there would be no need to use static mocks anymore. In Spock, the framework you used in your original version of the question, you would not even need Mockito anymore. Spock mocks would suffice. Another reason to make the static field non-final is lazy singleton initialisation. You could just initialise the field in the static getter, if unset. – kriegaex Dec 07 '22 at 07:04
  • You seem to be a huge fan of premature optimisation, being afraid of performance loss where probably you do not need to be. Because also in the class using the singleton, you store the instance in yet another static final field. Why don't you just call the getter when you need it? – kriegaex Dec 07 '22 at 07:07
  • One last thought: If you are on Java 9+ and are concerned about a protected field instead of a private one, you could always encapsulate your package in a Java module. Personally, I do not like JMS at all. To me it does not solve any problems I have but creates many. But it would certainly be possible. – kriegaex Dec 07 '22 at 07:09
  • kriegaex, To be honest, I don't quite understand that “ In Spock, the framework you used in your original version of the question, you would not even need Mockito anymore. Spock mocks would suffice. Another reason to make the static field non-final is lazy singleton initialisation. You could just initialise the field in the static getter, if unset”. If possible, I hope you can give a complete answer. Thanks – ppanda2021 Dec 07 '22 at 08:43
0

Thanks a lot for @kriegaex suggestions. I attemp to modify the demo and use spock to test it. and it works well when changing instance to be non-final.

public class DemoSingleTon {
    private static DemoSingleTon instance = new DemoSingleTon(); // <-- change to be non-final

    private DemoSingleTon() {
    }

    public static DemoSingleTon getInstance() {
        return instance;
    }

    public String test(String input) {
        return input == null ? "" : input;
    }
}
public class DemoMain {

    private static DemoSingleTon instance = DemoSingleTon.getInstance();

    public static String testInput() {
        return TestUtil.test("");
    }

    public String testInputUseSingleTone() {
        return instance.test("input1");
    }
}

spock test code as follows:

class DemoTest extends Specification {


    def "test"() {
        given:
        def demo = new DemoMain()

        expect:
        "input1" == demo.testInputUseSingleTone()
    }

    def "test1"() {
        given:
        def demoTon = Mock(DemoSingleTon.class)
        demoTon.test(_) >> "nothing"
        def demo = new DemoMain(demoSingleTon: demoTon)

        expect:
        "nothing" == demo.testInputUseSingleTone()
    }

    def "test2"() {
        given:
        def demoTon = Mock(DemoSingleTon.class)
        demoTon.test(_) >> "everything"
        def demo = new DemoMain(demoSingleTon: demoTon)

        expect:
        "everything" == demo.testInputUseSingleTone()
    }
}
ppanda2021
  • 15
  • 6
  • That you made the instance non-final has nothing to do with that it works like this. You are not accessing the singleton instance at all. You are simply stubbing a method of its mock. That is another use case than before. – kriegaex Dec 07 '22 at 14:12
  • I'm confused by your comment. It seems that my writing is not what you expect. If possible, I hope you can provide a complete demo. – ppanda2021 Dec 08 '22 at 03:42
0

Your own answer for Spock is not working for several reasons:

  1. The test uses new DemoMain(demoSingleTon : demoTon), but a field of that name does not exist. The existing constructor would be new DemoMain().
  2. After fixing that, both test1() and test2() fail with errors like Condition not satisfied: "nothing" == demo.testInputUseSingleTone().
  3. You are trying to mock class DemoSingleTon, which would not work because you cannot mock a final class.
  4. But mocking the singleton in (3) like this would not have any effect, because due to (1) you did not correctly inject it into DemoMain.
  5. You made DemoSingleTon.instance non-final, but then you do not use the option to inject a mock instance there.

There is more, but these are the things I noticed right away. If you post an answer here, make sure that it is not untested pseudo code. Otherwise, how can I improve it for you when you ask me to? You claim that your solution works, but for obvious reason that is impossible. I even tried in order to confirm it.

I also do not understand why you would store the result of DemoSingleTon.getInstance() in another static variable. All this premature optimisation makes your code harder to test. It is nice that you want to be diligent about things like memory consumption and performance, but are you really having a problem there? Maybe first you should get the code under test working correctly and then also the tests.

How about something like this?

package de.scrum_master.stackoverflow.q74699357;

public class MySingleton {
  protected static MySingleton instance = new MySingleton();

  private MySingleton() {}

  public static MySingleton getInstance() {
    return instance;
  }

  public String test(String input) {
    return input == null ? "" : input;
  }
}
package de.scrum_master.stackoverflow.q74699357;

public class MyMain {
  public String useSingleton() {
    return MySingleton.getInstance().test("input1");
  }
}
package de.scrum_master.stackoverflow.q74699357

import spock.lang.Specification

class MyMainTest extends Specification {
  def "real singleton"() {
    expect:
    new MyMain().useSingleton() == "input1"
  }

  def "mock singleton with '#expectedResult'"() {
    given:
    def originalSingleton = MySingleton.instance
    MySingleton.instance = Mock(MySingleton) {
      test(_) >> expectedResult
    }

    expect:
    new MyMain().useSingleton() == expectedResult

    cleanup:
    MySingleton.instance = originalSingleton

    where:
    expectedResult << ["nothing", "everything"]
  }
}

That would make a bit more sense, and the tests would actually pass:

IntelliJ IDEA test result

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • I'm sorry that I did't copy the final code.I've made some corrections to my answer, just changing DemoSingleTon non-final. The rest of the code remained as it is. And it worked. – ppanda2021 Dec 12 '22 at 06:19
  • Due to my lack of reputation, unfortunately I can't send pictures. All in all, your answer has benefited me. – ppanda2021 Dec 12 '22 at 06:34
  • No, `new DemoMain(demoSingleTon: demoTon)` still does not make any sense with the code you provided, because `demoSingleTon` is static. You just waste everybody's time here. My answer ist correct and tests something that makes sense. Yours is simply nonsencse, sorry for being so blunt. – kriegaex Dec 16 '22 at 11:34