Singleton and unit testing

前端 未结 12 1521
一生所求
一生所求 2021-01-30 10:24

The Effective Java has the following statement on unit testing singletons

Making a class a singleton can make it difficult to test its clients, as it’s im

相关标签:
12条回答
  • 2021-01-30 11:13

    durian-globals does lazy double-locked initialization of singletons, but also has a simple test-only API which allows you to replace the implementation for unit testing.

    0 讨论(0)
  • 2021-01-30 11:17

    Mocks require interfaces, because what you're doing is replacing the real underlying behavior with an imposter that mimics what you need for the test. Since the client only deals with an interface reference type, it doesn't need to know what the implementation is.

    You can't mock a concrete class without an interface, because you can't replace the behavior without the test client knowing about it. It's a completely new class in that case.

    It's true for all classes, Singleton or not.

    0 讨论(0)
  • 2021-01-30 11:21

    As far as I know, a class implementing a Singleton cannot be extended (superclass constructor is always called implicitly and the constructor in a Singleton is private). If you want to mock a class you have to extend the class. As you see in this case it wouldn't be possible.

    0 讨论(0)
  • 2021-01-30 11:22

    The problem isn't testing singletons themselves; the book is saying that if a class you are trying to test depends on a singleton, then you will likely have problems.

    Unless, that is, you (1) make the singleton implement an interface, and (2) inject the singleton to your class using that interface.

    For example, singletons are typically instantiated directly like this:

    public class MyClass
    {
        private MySingleton __s = MySingleton.getInstance() ;
    
        ...
    }
    

    MyClass may now be very difficult to automatedly test. For example, as @Boris Pavlović notes in his answer, if the singleton's behaviour is based on the system time, your tests are now also dependent on the system time, and you may not be able to test cases that, say, depend on the day of the week.

    However, if your singleton "implements an interface that serves as its type" then you can still use a singleton implementation of that interface, so long as you pass it in:

    public class SomeSingleton
        implements SomeInterface
    {
        ...
    }
    
    public class MyClass
    {
        private SomeInterface __s ;
    
        public MyClass( SomeInterface s )
        {
            __s = s ;
        }
    
        ...
    }
    
    ...
    
    MyClass m = new MyClass( SomeSingleton.getInstance() ) ;
    

    From the perspective of testing MyClass you now don't care if SomeSingleton is singleton or not: you can also pass in any other implementation you want, including the singleton implementation, but most likely you'll use a mock of some sort which you control from your tests.

    BTW, this is NOT the way to do it:

    public class MyClass
    {
        private SomeInterface __s = SomeSingleton.getInstance() ;
    
        public MyClass()
        {
        }
    
        ...
    }
    

    That still works out the same at run-time, but for testing you are now again dependent on SomeSingleton.

    0 讨论(0)
  • 2021-01-30 11:22
    it’s impossible to substitute a mock implementation for a singleton
    

    This is not true. You can subclass your singleton and setter inject a mock. Alternatively, you can use PowerMock to mock static methods. However the need to mock singletons can be symptomatic of poor design.

    The real problem is Singletons when abused turn into dependency magnets. Since they are accessible everywhere, it can appear more convenient to put the functions you need in them rather than delegating to an appropriate class, especially for programmers new to OOP.

    The testability problem is now you have a bunch of Singletons that are accessed by your object under test. Even though the object probably only uses a small fraction of methods in the Singletons, you still need to mock each Singleton and figure out which methods are depended on. Singletons with a static state (Monostate pattern) are even worse because you can have to figure out which interactions between objects are affected by the Singleton's state.

    Used carefully, Singletons and testability can occur together. For instance, in absence of a DI framework, you can use Singletons as your Factories and ServiceLocators, which you can setter inject to create a fake service layer for your end-to-end tests.

    0 讨论(0)
  • 2021-01-30 11:22

    It is possible, see the example

    import static org.junit.Assert.assertEquals;
    import static org.mockito.Mockito.atLeastOnce;
    import static org.mockito.Mockito.mock;
    import static org.mockito.Mockito.verify;
    import static org.mockito.Mockito.when;
    
    import java.lang.reflect.Field;
    
    import org.junit.After;
    import org.junit.Before;
    import org.junit.Test;
    
    public class DriverSnapshotHandlerTest {
    
    private static final String MOCKED_URL = "MockedURL";
    private FormatterService formatter;
    
    @SuppressWarnings("javadoc")
    @Before
    public void setUp() {
        formatter = mock(FormatterService.class);
        setMock(formatter);
        when(formatter.formatTachoIcon()).thenReturn(MOCKED_URL);
    }
    
    /**
     * Remove the mocked instance from the class. It is important, because other tests will be confused with the mocked instance.
     * @throws Exception if the instance could not be accessible
     */
    @After
    public void resetSingleton() throws Exception {
       Field instance = FormatterService.class.getDeclaredField("instance");
       instance.setAccessible(true);
       instance.set(null, null);
    }
    
    /**
     * Set a mock to the {@link FormatterService} instance
     * Throws {@link RuntimeException} in case if reflection failed, see a {@link Field#set(Object, Object)} method description.
     * @param mock the mock to be inserted to a class
     */
    private void setMock(FormatterService mock) {
        Field instance;
        try {
            instance = FormatterService.class.getDeclaredField("instance");
            instance.setAccessible(true);
            instance.set(instance, mock);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * Test method for {@link com.example.DriverSnapshotHandler#getImageURL()}.
     */
    @Test
    public void testFormatterServiceIsCalled() {
        DriverSnapshotHandler handler = new DriverSnapshotHandler();
        String url = handler.getImageURL();
    
        verify(formatter, atLeastOnce()).formatTachoIcon();
        assertEquals(MOCKED_URL, url);
    }
    
    }
    
    0 讨论(0)
提交回复
热议问题