测试需要模拟成员方法并使用 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 一个模拟对象)。

我使用的 PowerMockitohamcrestJUnit 的版本列在这里一个参考:

<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重命名自动化功能在这种情况下不会有效。