Search code examples
javaunit-testingmockingmockitopowermockito

Testing a static method which needs to mock a member method and suppress singleton constructor with PowerMockito


I would like to test on a static method (e.g. HobbyUtil.java:shareReadContext(int lineNumber) ) which would ask a Singleton class (e.g. Shelf.java ) to get an object (e.g. a Book ) for further retrieving values (e.g. lines of context ) from that returned object.

However, during the mock case execution, the ctor of that Singleton class (i.e. Shelf.java ) would trigger several other Singleton classes (e.g. LibraryA.java ) in a chain; and one of them would trigger java.lang.ExceptionInInitializerError.

I think mocking/spying those irrelevant Singleton classes is out of the spirit of Mock Test as they are irrelevant to my testing scope.

So I decided to suppress the private ctor of this Singleton class (i.e. Shelf.java ) and just let the getter method in this class (i.e. getBook() ) to return a mocked object with my preferred behavior, which would perfectly match my test case.

The problem I've encountered is that I follow this link to suppress the ctor of Shelf.java successfully: suppress a singleton constructor in java with powermock

but I couldn't figure out the way to let this getter method return what I want properly (i.e. return a mocked object).

The version of PowerMockito, hamcrest and JUnit I am using are listed here for a reference:

<dependency><groupId>org.powermock</groupId><artifactId>powermock-module-junit4</artifactId><version>1.7.0</version><scope>test</scope></dependency>  
<dependency><groupId>org.powermock</groupId><artifactId>powermock-api-mockit02</artifactId><version>1.7.0</version><scope>test</scope></dependency>
<dependency><groupId>org.hamcrest</groupId><artifactId>hamcrest-all</artifactId><version>1.3</version><scope>test</scope></dependency>
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency>

I have also replicated the case with below sample classes to show what I have encountered (HobbyUtilTest.java is the class for my test case):

Supposed there are 5 Java classes in the application:

public interface Book {
    public String getContext(int lineNumber);
}
public class Shelf {

    private static final Shelf shelf = new Shelf();
    private String BOOK_NAME = "HarryPotter";
    private Book book = null;

    private Shelf() {
        book = LibraryA.getInstance().getBook(BOOK_NAME);
        // many relatively complicated logics are here in actual class ... 
        // I want to suppress this ctor 
    }

    public static Shelf getInstance() {
        return shelf;
    }

    public Book getBook() {
        return book;  // I want to return my mocked object while calling this method in test case
    }

}
public class HobbyUtil {

    public static String shareReadContext(int lineNumber){
        String context = "";
        Book book = Shelf.getInstance().getBook();

        for (int i = 0 ; i < lineNumber; i++) {
            context += book.getContext(i);
        }
        return context;
    }

    private HobbyUtil() {}
}
public class LibraryA {
    private static final LibraryA library = new LibraryA();
    private Book book;

    private LibraryA() {
        throw new java.lang.ExceptionInInitializerError();
    }

    public static LibraryA getInstance() {
        return library;
    }

    public Book getBook(String bookName) {
        return book;
    }
}
public class BookImpl implements Book{

    private String bookName = null;

    BookImpl(String bookName){
        this.bookName = bookName;
    }

    @Override
    public String getContext(int lineNumber) {
        return lineNumber + ": Context";
    }
}

And I would test the static method shareReadContext(int lineNumber) in HobbyUtil.java with HobbyUtilTest.java

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.powermock.api.mockito.PowerMockito.when;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest(value = { Shelf.class })
public class HobbyUtilTest {

    @Test
    public void testShareReadContext() throws Exception {

        Book mockBook = PowerMockito.mock(Book.class);
        when(mockBook.getContext(anyInt())).thenReturn("context for test");

        PowerMockito.suppress(PowerMockito.constructor(Shelf.class));
        //PowerMockito.spy(Shelf.class);
        //when(Shelf.getInstance().getBook()).thenReturn(mockBook); // does not work
        //PowerMockito.doReturn(mockBook).when(Shelf.getInstance().getBook()); // does not work
        //PowerMockito.when(Shelf.class, "getBook").thenReturn(mockBook); // does not work
        //TODO any approach for it?

        String context = HobbyUtil.shareReadContext(1);
        assertThat(context, is("context for test"));
    }
}

Could anyone please help to suggest how could I let the line Book book = Shelf.getInstance().getBook(); in HobbyUtil.java return my mocked object (i.e. mockBook )?


Solution

  • Assuming that your Shelf class is in a package test, this should work:

    @RunWith(PowerMockRunner.class)
    @SuppressStaticInitializationFor("test.Shelf")
    public class HobbyUtilTest {
    
        @Test
        public void testShareReadContext() throws Exception {
    
            Book book = Mockito.mock(Book.class);
            Mockito.when(book.getContext(Mockito.anyInt())).thenReturn("context for test");
    
            Shelf shelf = Mockito.mock(Shelf.class);
            Mockito.when(shelf.getBook()).thenReturn(book);
    
            PowerMockito.mockStatic(Shelf.class);
            PowerMockito.when(Shelf.getInstance()).thenReturn(shelf);
    
            String context = HobbyUtil.shareReadContext(1);
            Assert.assertEquals("context for test", context);
        }
    }
    

    You'll want to suppress the initialization of the fields of Shelf (not only the constructor) and afterwards simply define the appropriate mocks for the Shelf.getInstance() (and subsequent) methods.


    Ps.:

    @SuppressStaticInitializationFor seems to be equivalent to:

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Shelf.class)
    public class HobbyUtilTest {
    
        @Test
        public void testShareReadContext() throws Exception {
    
            PowerMockito.suppress(PowerMockito.fields(Shelf.class));
            PowerMockito.suppress(PowerMockito.constructor(Shelf.class));       
    
            // ...
        }
    }