测试一个静态方法,该方法需要使用 PowerMockito 模拟成员方法并抑制单例构造函数



我想在静态方法上进行测试(例如HobbyUtil.java:shareReadContext(int lineNumber)),这将询问一个单例类(例如Shelf.java)获取一个对象(例如Book),以便从该返回对象中进一步检索值(例如上下文)。

但是,在模拟案例执行期间,该单例类的 ctor (即Shelf.java)将触发其他几个单例类(例如LibraryA.java) 在链条中;其中一个会触发java.lang.ExceptionInInitializerError.

我认为嘲笑/监视那些不相关的单例类是出于模拟测试的精神,因为它们与我的测试范围无关。

所以我决定压制这个单例类的私人 ctor(即Shelf.java),并且只让 getter 方法在此类中(即getBook()) 返回一个具有我首选行为的模拟对象,这将完全匹配我的测试用例。

我遇到的问题是我点击此链接成功抑制了货架.java的 ctor: 使用 PowerMock 在 Java 中抑制单例构造函数

但是我想不出让这个getter方法正确返回我想要的内容(即返回一个模拟对象)的方法。

我正在使用的PowerMockito,hamcrestJUnit的版本在这里列出以供参考:

<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>

我还用下面的示例类复制了这个案例,以展示我遇到的问题(HobbyUtilTest.java是我的测试用例的类):

假设应用程序中有 5 个 Java 类:

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";
}
}

我会用HobbyUtilTest.java测试静态方法shareReadContext(int lineNumber)HobbyUtil.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"));
}
}

谁能帮忙建议我如何让行Book book = Shelf.getInstance().getBook();HobbyUtil.java返回我的模拟对象(即mockBook)?

假设您的Shelf类在包test中,这应该可以工作:

@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);
}
}

您需要禁止初始化Shelf字段(不仅是构造函数),然后只需为Shelf.getInstance()(和后续)方法定义适当的模拟。


附言:

@SuppressStaticInitializationFor似乎等同于:

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

让我在这里发布一种替代方法,以便在同时再次搜索其他资源后参考。

引用此站点后: https://blog.jayway.com/2009/10/28/untestable-code-with-mockito-and-powermock/

我发现通过将以下代码行添加到原始测试用例的TODO部分也有助于实现目的:

PowerMockito.stub(PowerMockito.method(Shelf.class, "getBook")).toReturn(mockBook);

这种方法的缺点是方法名称需要硬编码为字符串值,在这种情况下,IDE 重命名自动化功能将无效。

最新更新