Why is a singleton class hard to test?
Asked Answered
S

4

19

Effective Java Item 3 (Enforce the singleton property with a private constructor or an enum type) notes that:

Making a class a singleton can make it difficult to test its clients, as it's impossible to substitute a mock implementation for a singleton unless it implements an interface that serves as its type.

For testing purposes, why is it not sufficient to instantiate the one singleton instance and test its APIs? Isn't that what a client would be consuming? The quote seems to imply that testing the singleton would involve a "mock implementation," but why is that necessary?

I've seen various "explanations" that are more or less rephrasings of the quote above. Can someone explain this further, preferably with a code example?

Sinful answered 12/8, 2014 at 20:8 Comment(4)
The quote isn't about testing the singleton, it's about testing code that uses the singleton.Jasmin
Now do you test the client? In a unit test you need to mock dependencies and test only the CUT. How do you do this when the client uses a hardcoded reference to Singleton.INSTANCE?Spiteful
This actually is true for statics in general (that is, anything accessed in a static manner)Som
Since nowadays a singleton gets injected and is not accessed in a static manner, do not try too hard on this statement.Bloodsucker
X
22

What if your singleton was performing operations on a database or writing data to a file? You would not want that occurring in a unit test. You would want to mock out the object to perform some operations in memory instead so you could verify them without having permanent side effects. Unit tests should be self contained and should not create connections to databases or perform other operations with outside systems that could fail and then cause your unit test to fail for an unrelated reason.

Example with pseudo-java (I'm a C# dev):

public class MySingleton {

    private static final MySingleton instance = new MySingleton();

    private MySingleton() { }

    public int doSomething() {
        //create connection to database, write to a file, etc..
        return something;
    }

    public static MySingleton getInstance() {
        return instance;
    }
}

public class OtherClass {

        public int myMethod() {
            //do some stuff
            int result = MySingleton.getInstance().doSomething();

            //do some other suff
            return something;
        }
}

In order to test myMethod we have to make an actual database call, file operation etc

@Test
public void testMyMethod() {
    OtherClass obj = new OtherClass();

    //if this fails it might be because of some external code called by 
    //MySingleton.doSomething(), not necessarily the logic inside MyMethod()

    Asserts.assertEqual(1, obj.myMethod());
}

If MySingleton was instead something like:

public class MyNonSingleton implements ISomeInterface {

    public MyNonSingleton() {}

    @Override
    public int doSomething() {
        //create connection to database, write to a file, etc..
        return something;
    }

}

you could then inject it as a dependency into MyOtherClass like this:

public class OtherClass {

    private ISomeInterface obj;

    public OtherClass(ISomeInterface obj) {
        this.obj = obj;
    }

    public int myMethod() {
        //do some stuff
        int result = obj.doSomething();

        //do some other stuff
        return something;
    }
}

then you can test like this:

@Test
public void TestMyMethod() {
    OtherClass obj = new OtherClass(new MockNonSingleton());

    //now our mock object can fake the database, filesystem etc. calls to isolate the testing to just the logic in myMethod()

    Asserts.assertEqual(1, obj.myMethod());
}
Xanthus answered 12/8, 2014 at 20:12 Comment(7)
But it seems like the main difference between MySingleton and MyNonSingleton is that MyNonSingleton implements an interface that serves as the type, not that MyNonSingleton isn't a singleton. If MySingleton were to remain a singleton, but implemented ISomeInterface, wouldn't you be able to test it in the same way (with a MockSingleton that implemented the same interface)?Sinful
@Sinful But how are you going to change which singleton is used in your unit tests?Xanthus
@Xanthus I Java'fied it a bit for you :) To contribute to the general discussion. There is powermock that enables you to mock even static methods. Thus Josh's point on testability is less of a problem when you choose to use it.Iambic
@ThomasJungblut Thanks. It's been a while since I used Java.Xanthus
@Sinful wouldn't a singleton by definition be the only version? Otherwise it's at best a dualton ;)Subpoena
@Xanthus ,I think "MyNonSingleton" can be replace with something like MySingletonImpl ? It doesn't matter whether singleton or not.Placate
@Subpoena on coder123 's question.... if its implementing an interface, how would you create another instance just with the interface? On another hand to coder123 's original question, so just to test the singleton, do we need to implement an interface??Festa
A
8

Personally I think this statement is totally wrong, because it assumes that singleton is not replacable (mockable) for unit tests. On the contrary. In Spring's dependency injection, for example, singleton is actually the default model for DI components. Singletons and dependency injection are not mutually exclusive, which statement above somehow tries to imply.

I agree that anything that can't be mocked makes application more difficult to test, but there is no reason to assume singletons are less mockable than any other objects in your application.

What might be the problem, is the fact that singleton is one global instance and when it can be in too many different states, unit tests might show unpredictable results because of changing state of the singleton. But there are simple solutions to this - mock your singleton and make your mock to have less states. Or write your tests in such a fashion, that singleton is recreated (or reinitialized) before each unit test that depends on it. Or, the best solution, test your application for all possible states of the singleton. Ultimately, if reality requires multiple states, like, for example, a database connection (disconnected/connecting/connected/error/...), then you will have to deal with it regardless of whether you use singletons or not.

Avocation answered 28/1, 2019 at 12:26 Comment(0)
B
0

Impossible to substitute a mock implementation for a singleton

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

public class Main {
    @Test
    void test(){
        SpellChecker s = Mockito.mock(SpellChecker.class); //IMPOSSIBLE
        when(s.check(any())).thenReturn(false);
        Client c = new Client(s);
        assertThat(c.check("abc")).isEqualTo(false);
    }
}

class SpellChecker{
    private static final SpellChecker INSTANCE = new SpellChecker();
    private SpellChecker(){throw new AssertionError();}
    public boolean check(String word){return true;}
    public static SpellChecker getInstance(){return INSTANCE;}
}

class Client{
    private SpellChecker s;
    Client(SpellChecker s){this.s=s;}
    boolean check(String str){return s.check(str);}
}

unless it implements an interface that serves as its type.

public class Main {
    @Test
    void test(){
        SpellCheckerI s = Mockito.mock(SpellCheckerI.class); //POSSIBLE
        when(s.check(any())).thenReturn(false);
        Client c = new Client(s);
        assertThat(c.check("abc")).isEqualTo(false);
    }
}

interface SpellCheckerI{boolean check(String word);}

class SpellChecker implements SpellCheckerI{
    private static final SpellChecker INSTANCE = new SpellChecker();
    private SpellChecker(){throw new AssertionError();}
    @Override public boolean check(String word){return true;}
    public static SpellChecker getInstance(){return INSTANCE;}
}

class Client{
    private SpellCheckerI s;
    Client(SpellCheckerI s){this.s=s;}
    boolean check(String str){return s.check(str);}
}

P.S You might want to check out this wonderful post too. Btw, the unit test state danger is not a great example(it's not a unit test), but getting the point is more important.

Browband answered 10/1, 2022 at 1:24 Comment(0)
W
0

You can actually substitute a mocked implementation for a singleton if you wanted to

import static org.mockito.Mockito.mockConstruction;

try (MockedConstruction<SpellChecker> mocked = mockConstruction(SpellChecker.class,
            (mock, context) -> {
               // other mock and when-thenReturn constructs
    
            })) {
    
        // assertions here
    }
Weylin answered 30/5, 2023 at 16:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.