测试需要模拟成员方法并使用 PowerMockito 抑制单例构造函数的静态方法
Testing a static method which needs to mock a member method and suppress singleton constructor with PowerMockito
我想测试一个静态方法(例如 HobbyUtil.java:shareReadContext(int lineNumber)
),它会要求单例 class (例如 Shelf.java
)获取一个对象(例如 Book
) 以进一步从 returned 对象中检索值(例如 上下文行 )。
然而,在模拟案例执行期间,该单例 class 的 ctor(即 Shelf.java
)将触发其他几个单例 classes(例如 LibraryA.java
)在链中;其中之一会触发 java.lang.ExceptionInInitializerError
。
我认为mocking/spying那些不相关的单例class不符合模拟测试的精神,因为它们与我的测试范围无关。
所以我决定抑制这个 Singleton class(即 Shelf.java
)的私有 ctor,只让 class 中的 getter 方法(即 getBook()
) 到 return 具有我首选行为的模拟对象,这将完美匹配我的测试用例。
我遇到的问题是,我按照这个link成功压制了Shelf.java的ctor:
suppress a singleton constructor in java with powermock
但我想不出让这个 getter 方法 return 我想要的东西的方法(即 return 一个模拟对象)。
我使用的 PowerMockito、hamcrest 和 JUnit 的版本列在这里一个参考:
<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>
我还用下面的示例 classes 复制了案例,以展示我遇到的情况(HobbyUtilTest.java 是我的测试案例的 class):
假设申请中有5个Java class:
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
测试 HobbyUtil.java
中的静态方法 shareReadContext(int lineNumber)
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"));
}
}
任何人都可以帮助建议我如何让 HobbyUtil.java
return 中的行 Book book = Shelf.getInstance().getBook();
我的模拟对象(即 mockBook
)?
假设您的 Shelf
class 在包裹 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()
(和后续)方法定义适当的模拟。
Ps.:
@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));
// ...
}
}
在此期间再次搜索其他资源后,让我post这里提供一种替代方法以供参考。
引用此站点后:
https://blog.jayway.com/2009/10/28/untestable-code-with-mockito-and-powermock/
我发现在原测试用例的TODO
段加入如下代码也可以达到目的:
PowerMockito.stub(PowerMockito.method(Shelf.class,
"getBook")).toReturn(mockBook);
这种方法的缺点是方法名称需要硬编码为字符串值,IDE重命名自动化功能在这种情况下不会有效。
我想测试一个静态方法(例如 HobbyUtil.java:shareReadContext(int lineNumber)
),它会要求单例 class (例如 Shelf.java
)获取一个对象(例如 Book
) 以进一步从 returned 对象中检索值(例如 上下文行 )。
然而,在模拟案例执行期间,该单例 class 的 ctor(即 Shelf.java
)将触发其他几个单例 classes(例如 LibraryA.java
)在链中;其中之一会触发 java.lang.ExceptionInInitializerError
。
我认为mocking/spying那些不相关的单例class不符合模拟测试的精神,因为它们与我的测试范围无关。
所以我决定抑制这个 Singleton class(即 Shelf.java
)的私有 ctor,只让 class 中的 getter 方法(即 getBook()
) 到 return 具有我首选行为的模拟对象,这将完美匹配我的测试用例。
我遇到的问题是,我按照这个link成功压制了Shelf.java的ctor: suppress a singleton constructor in java with powermock
但我想不出让这个 getter 方法 return 我想要的东西的方法(即 return 一个模拟对象)。
我使用的 PowerMockito、hamcrest 和 JUnit 的版本列在这里一个参考:
<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>
我还用下面的示例 classes 复制了案例,以展示我遇到的情况(HobbyUtilTest.java 是我的测试案例的 class):
假设申请中有5个Java class:
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
HobbyUtil.java
中的静态方法 shareReadContext(int lineNumber)
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"));
}
}
任何人都可以帮助建议我如何让 HobbyUtil.java
return 中的行 Book book = Shelf.getInstance().getBook();
我的模拟对象(即 mockBook
)?
假设您的 Shelf
class 在包裹 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()
(和后续)方法定义适当的模拟。
Ps.:
@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));
// ...
}
}
在此期间再次搜索其他资源后,让我post这里提供一种替代方法以供参考。
引用此站点后: https://blog.jayway.com/2009/10/28/untestable-code-with-mockito-and-powermock/
我发现在原测试用例的TODO
段加入如下代码也可以达到目的:
PowerMockito.stub(PowerMockito.method(Shelf.class, "getBook")).toReturn(mockBook);
这种方法的缺点是方法名称需要硬编码为字符串值,IDE重命名自动化功能在这种情况下不会有效。