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
)?
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));
// ...
}
}