使用PowerMock进行单元测试

1,595 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 11 天,点击查看活动详情

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

PowerMock是一个单元测试打桩框架,可以模拟静态方法私有方法final方法等来简化单元测试的编写。本篇文章将结合简单例子对PowerMock的常用方法进行说明。

准备工作

一. 注解添加与使用场景

在使用PowerMock时需要针对不同场景添加对应注解,主要是@RunWith和@PrepareForTest注解。注解添加和场景对应如下所示。

场景注解
模拟final方法@PrepareForTest,@RunWith
模拟静态方法@PrepareForTest,@RunWith
模拟私有方法@PrepareForTest
使用whenNew@PrepareForTest,@RunWith

@PrepareForTest注解用于告诉PowerMock需要准备某些类进行测试,这些类包括final类,带有finalprivatestaticnative方法的类,上述类需要PowerMock基于字节码进行操作。可以按照@PrepareForTest(TestObj.class) 的方式来告诉PowerMock准备TestObj类进行测试。

@RunWith注解用于告诉JUnit使用哪个运行类来运行@RunWith注解修饰的类中的测试程序,例如@RunWith(PowerMockRunner.class),则告诉JUnit使用PowerMockRunner来运行@RunWith注解修饰的类中的测试程序。

二. 使用PowerMock需要添加的依赖

需要引入的依赖如下所示。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.23.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>

引入mockito-core是为了提供Mockito功能,主要使用到org.mockito.ArgumentMatchers参数占位符,部分情况需要使用到org.mockito.BDDMockito。引入powermock-api-mockito2powermock-module-junit4是为了提供PowerMock功能,其中powermock-module-junit4中还引入了hamcrest-core,主要是使用其提供的org.hamcrest.MatcherAssert.assertThatorg.hamcrest.Matchers.is进行断言判断。

在引入依赖时,需要注意核对MockitoPowerMock的版本对应关系,否则会报java.lang.ClassNotFoundException: org.mockito.exceptions.Reporter错误。版本对应关系可以去PowerMock官网进行查询:PowerMock官网,通常情况下,如果引入的mockito-core版本为2.x,则PowerMockapi需要使用powermock-api-mockito2

正文

一. Mock公共方法

如下是一个有一个公共方法的待Mock类。

public class MockPublicMethod {

    public boolean isTrue() {
        return true;
    }

}

如下是使用PowerMockMockPublicMethod进行打桩,让MockPublicMethod#isTrue方法返回false

public class PowerMockTest {

    @Test
    public void mockPublic() {
        MockPublicMethod mockPublicMethod = PowerMockito
                .mock(MockPublicMethod.class);
        PowerMockito.when(mockPublicMethod.isTrue()).thenReturn(false);
        assertThat(mockPublicMethod.isTrue(), is(false));
    }

}

Mock公共方法时需要使用PowerMockito.mock(方法所在类.class) 获取Mock出来的对象,这里称之为mock实例,mock实例的方法均为假方法,不对mock实例进行任何操作的情况下,调用mock实例的方法会返回(如果有返回值的话)返回值类型的默认值(零值,比如String返回nullInteger返回0)。如果想要调用mock实例的方法时使其执行真实方法,那么打桩时需要使用thenCallRealMethod(),如下所示。

public class MockPublicMethod {

    public boolean isTrue() {
        return true;
    }

}

public class PowerMockTest {

    @Test
    public void mockPublic() {
        MockPublicMethod mockPublicMethod = PowerMockito
                .mock(MockPublicMethod.class);
        PowerMockito.when(mockPublicMethod.isTrue()).thenCallRealMethod();
        assertThat(mockPublicMethod.isTrue(), is(true));
    }

}

同时可以使用whenNew() 来实现在程序中new一个对象时得到一个mock实例。如下所示。

public class MockPublicMethod {

    public boolean isTrue() {
        return true;
    }

}

public class TestObj {

    public boolean isTrue() {
        MockPublicMethod mockPublicMethod = new MockPublicMethod();
        return mockPublicMethod.isTrue();
    }

}

@RunWith(PowerMockRunner.class)
@PrepareForTest(TestObj.class)
public class PowerMockTest {

    @Test
    public void mockWhenNew() throws Exception {
        MockPublicMethod mockPublicMethod = PowerMockito.mock(MockPublicMethod.class);
        PowerMockito.when(mockPublicMethod.isTrue()).thenReturn(false);
        PowerMockito.whenNew(MockPublicMethod.class).withAnyArguments()
                .thenReturn(mockPublicMethod);
        TestObj testObj = new TestObj();
        assertThat(testObj.isTrue(), is(false));
    }

}

特别注意:在使用whenNew() 时,@PrepareForTest注解中一定得是被测试类的Class对象。上面例子中,在被测试类TestObj的方法中new了一个Mock类的对象,所以在@PrepareForTest注解中加入了TestObj.class,此时whenNew() 才会在TestObj中生效。

二. Mock Final公共方法

final公共方法进行Mock,基本与Mock公共方法一致,不过由于需要对final公共方法进行字节码操作以及需要使用PowerMockRunner来运行测试程序,因此需要在测试类上添加@RunWith和@PrepareForTest注解。

如下是被测试类。

public class MockFinnalPublicMethod {

    public boolean isTrue() {
        return true;
    }

}

测试类如下所示。

@RunWith(PowerMockRunner.class)
@PrepareForTest(MockFinnalPublicMethod.class)
public class PowerMockTest {

    @Test
    public void mockFinalPublic() {
        MockFinnalPublicMethod mockFinnalPublicMethod = PowerMockito
                .mock(MockFinnalPublicMethod.class);
        PowerMockito.when(mockFinnalPublicMethod.isTrue()).thenReturn(false);
        assertThat(mockFinnalPublicMethod.isTrue(), is(false));
    }

}

三. Mock私有方法

被测试类如下所示。

public class MockPrivateMethod {

    public boolean isTrue() {
        return returnTrue();
    }

    private boolean returnTrue() {
        return true;
    }

}

被测试类中有一个公共方法isTrue(),在isTrue() 方法中会调用MockPrivateMethod的私有方法returnTrue()。测试类如下所示。

@RunWith(PowerMockRunner.class)
@PrepareForTest(MockPrivateMethod.class)
public class PowerMockTest {

    @Test
    public void mockPrivate() throws Exception {
        MockPrivateMethod mockPrivateMethod = PowerMockito.mock(MockPrivateMethod.class);
        PowerMockito.when(mockPrivateMethod, "returnTrue").thenReturn(false);
        PowerMockito.when(mockPrivateMethod.isTrue()).thenCallRealMethod();
        assertThat(mockPrivateMethod.isTrue(), is(false));
    }

}

Mock私有方法打桩时,需要使用PowerMockito.when(mock实例, "私有方法名").thenReturn(期望返回值) 的形式设置mock实例的私有方法的返回值,如果私有方法有参数,还需要在私有方法名后面添加参数占位符,比如PowerMockito.when(mock实例, "私有方法名", anyInt()).thenReturn(期望返回值)。上面例子中进行断言时,调用私有方法采取了调用公共方法来间接调用私有方法的形式,单元测试代码对业务代码造成了入侵,因此如果仅仅只是为了验证一个私有方法,可以使用Whitebox来方便的调用私有方法,如下所示。

public class MockPrivateMethod {

    private boolean returnTrue() {
        return true;
    }

}

@RunWith(PowerMockRunner.class)
@PrepareForTest(MockPrivateMethod.class)
public class PowerMockTest {

    @Test
    public void mockPrivate() throws Exception {
        MockPrivateMethod mockPrivateMethod = PowerMockito.mock(MockPrivateMethod.class);
        PowerMockito.when(mockPrivateMethod, "returnTrue").thenReturn(false);
        assertThat(Whitebox.invokeMethod(mockPrivateMethod, "returnTrue"),
                is(false));
    }

}

四. Mock静态公共方法

如下是被测试类,有一个静态公共方法,如下所示。

public class MockStaticPublicMethod {

    public static boolean isTrue() {
        return true;
    }

}

测试类如下所示。

@RunWith(PowerMockRunner.class)
@PrepareForTest(MockStaticPublicMethod.class)
public class PowerMockTest {

    @Test
    public void mockStaticPublic() {
        PowerMockito.mockStatic(MockStaticPublicMethod.class);
        PowerMockito.when(MockStaticPublicMethod.isTrue()).thenReturn(false);
        assertThat(MockStaticPublicMethod.isTrue(), is(false));
    }

}

对静态方法Mock时,首先需要在测试类上添加@RunWith和@PrepareForTest注解,同时需要调用PowerMockitomockStatic() 方法完成对静态方法所在类的Mock,后续才能通过PowerMockito改变静态方法的行为。

五. Mock静态私有方法

被测试类如下所示。

public class MockStaticPrivateMethod {

    public static boolean isTrue() {
        return returnTrue();
    }

    private static boolean returnTrue() {
        return true;
    }

}

被测试类中有一个静态公共方法isTrue(),在isTrue() 方法中会调用MockStaticPrivateMethod的静态私有方法returnTrue()。测试程序如下所示。

@RunWith(PowerMockRunner.class)
@PrepareForTest(MockStaticPrivateMethod.class)
public class PowerMockTest {

    @Test
    public void mockStaticPrivate() throws Exception {
        PowerMockito.mockStatic(MockStaticPrivateMethod.class);
        PowerMockito.when(MockStaticPrivateMethod.class, "returnTrue")
                .thenReturn(false);
        PowerMockito.when(MockStaticPrivateMethod.isTrue()).thenCallRealMethod();
        assertThat(MockStaticPrivateMethod.isTrue(), is(false));
    }

}

同样测试代码对业务代码造成了入侵,可以使用Whitebox来方便的调用静态私有方法,如下所示。

public class MockStaticPrivateMethod {

    private static boolean returnTrue() {
        return true;
    }

}

@RunWith(PowerMockRunner.class)
@PrepareForTest(MockStaticPrivateMethod.class)
public class PowerMockTest {

    @Test
    public void mockStaticPrivate() throws Exception {
        PowerMockito.mockStatic(MockStaticPrivateMethod.class);
        PowerMockito.when(MockStaticPrivateMethod.class, "returnTrue")
                .thenReturn(false);
        assertThat(Whitebox.invokeMethod(MockStaticPrivateMethod.class, "returnTrue"),
                is(false));
    }

}

六. Whitebox使用

1. 设置对象私有字段

使用Whitebox可以方便的设置对象(静态)私有字段值。被测试类如下所示。

public class WhiteboxHelp {

    private boolean flag = true;

    public boolean isTrue() {
        return flag;
    }

}

被测试类WhiteboxHelp有一个私有字段flag,同时WhiteboxHelpisTrue() 方法会返回flag的值。测试类如下所示。

public class PowerMockTest {

    @Test
    public void whiteboxPrivateField() {
        WhiteboxHelp whiteboxHelp = new WhiteboxHelp();
        Whitebox.setInternalState(whiteboxHelp, "flag", false);
        assertThat(whiteboxHelp.isTrue(), is(false));
    }

}

仅使用Whitebox时不需要添加@RunWith和@PrepareForTest注解,同时对于上面例子如果flag是静态变量,那么设置静态变量值时需要使用Whitebox.setInternalState(WhiteboxHelp.class, "flag", false)

特别注意:如果WhiteboxHelpflag字段是静态的,则无法使用Whitebox设置flag字段的值。

2. 调用私有方法

使用Whitebox也可以方便的调用对象(静态)私有方法。被测试类如下所示。

public class WhiteboxHelp {

    private boolean isTrue() {
        return true;
    }

}

测试类如下所示。

public class PowerMockTest {

    @Test
    public void whiteboxPrivateMethod() throws Exception {
        WhiteboxHelp whiteboxHelp = new WhiteboxHelp();
        assertThat(Whitebox.invokeMethod(whiteboxHelp, "isTrue"), is(true));
    }

}

对于上面例子,如果isTrue() 是静态私有方法,那么调用静态私有方法时的语句为:assertThat(Whitebox.invokeMethod(WhiteboxHelp.class, "isTrue"), is(true))

七. Answer-Mock

针对同一方法多次被调用且不同入参需要Mock不同出参的情况,可以使用Answer

被测试类如下所示。

public class AnswerHelp {

    public String convert(int num) {
        return StringUtils.EMPTY;
    }

}

测试类如下所示。

@RunWith(PowerMockRunner.class)
@PrepareForTest(AnswerHelp.class)
public class PowerMockTest {

    @Test
    public void answer() {
        AnswerHelp answerHelp = PowerMockito.mock(AnswerHelp.class);
        Answer<String> answer = new Answer<String>() {
            @Override
            public String answer(InvocationOnMock invocation) {
                int num = (Integer) invocation.getArguments()[0];
                if (num == 0) {
                    return "zero";
                } else if (num == 1) {
                    return "one";
                }
                return StringUtils.EMPTY;
            }
        };
        PowerMockito.when(answerHelp.convert(anyInt())).thenAnswer(answer);
        assertThat(answerHelp.convert(0), is("zero"));
        assertThat(answerHelp.convert(1), is("one"));
    }

}

其中Answer的泛型类型需要与answer() 方法的返回值类型一致,且通过InvocationOnMockgetArguments() 可以获取mock实例调用的方法所有入参,callRealMethod() 可以调用真实方法,getMethod() 可以获取mock实例调用的方法,getMock() 可以获取mock实例。同时,还可以使用org.mockito.BDDMockito.given来实现相同的效果,如下所示。

public class AnswerHelp {

    public String convert(int num) {
        return StringUtils.EMPTY;
    }

}

@RunWith(PowerMockRunner.class)
@PrepareForTest(AnswerHelp.class)
public class PowerMockTest {

    @Test
    public void answer() {
        AnswerHelp answerHelp = PowerMockito.mock(AnswerHelp.class);
        Answer<String> answer = new Answer<String>() {
            @Override
            public String answer(InvocationOnMock invocation) {
                int num = (Integer) invocation.getArguments()[0];
                if (num == 0) {
                    return "zero";
                } else if (num == 1) {
                    return "one";
                }
                return StringUtils.EMPTY;
            }
        };
        given(answerHelp.convert(anyInt())).willAnswer(answer);
        assertThat(answerHelp.convert(0), is("zero"));
        assertThat(answerHelp.convert(1), is("one"));
    }

}

八. Spy公共方法

先给出一个示例,再对Spy进行解释。被测试类如下所示。

public class SpyPublicMethod {

    public boolean isTrue1() {
        return true;
    }

    public boolean isTrue2() {
        return true;
    }

}

测试类如下所示。

public class PowerMockTest {

    @Test
    public void spyPublic() {
        SpyPublicMethod spy = PowerMockito.spy(new SpyPublicMethod());
        PowerMockito.doReturn(false).when(spy).isTrue1();
        assertThat(spy.isTrue1(), is(false));
        assertThat(spy.isTrue2(), is(true));
    }

}

Spy公共方法时需要使用PowerMockito.spy(方法所在类的实例) 获取Spy出来的对象,这里称之为spy实例,不对spy实例进行任何操作的情况下,spy实例与真实实例是完全一样的。同时由于spy实例与真实实例完全一样,因此在对spy实例进行打桩时使用doReturn()thenReturn() 是存在差别的:使用doReturn(返回值) 时不会执行真实方式,直接返回返回值;使用thenReturn(返回值) 时会先执行一遍真实方法,然后返回返回值。通常情况下Spy需要配合doReturn() 使用,用于抑制真实方法的执行,防止执行真实方法时报错。

同时,打桩时使用doReturn()thenReturn() 的语法存在差别,上面例子中打桩时如果使用的语句为PowerMockito.doReturn(false).when(spy.isTrue1()),会导致编译时正常,运行时报错的现象。下表对打桩时doReturn()thenReturn() 的语法进行了对比。

使用场景doReturn()thenReturn()
打桩public方法PowerMockito.doReturn(false).when(spy).isTrue()PowerMockito.when(spy.isTrue()).thenReturn(false)
打桩private方法PowerMockito.doReturn(false).when(spy, "returnTrue")PowerMockito.when(spy, "returnTrue").thenReturn(false)
打桩static方法PowerMockito.doReturn(false).when(SpyStaticPublicMethod.class, "isTrue")PowerMockito.when(SpyStaticPublicMethod.isTrue()).thenReturn(false)
打桩static private方法PowerMockito.doReturn(false).when(SpyStaticPrivateMethod.class, "returnTrue")PowerMockito.when(SpyStaticPrivateMethod.class, "returnTrue").thenReturn(false)

最后,Spy也和Mock一样,可以配合whenNew() 进行使用。

九. Spy私有方法

被测试类如下如下所示。

public class SpyPrivateMethod {

    public boolean isTrue() {
        return returnTrue();
    }

    public boolean returnTrue() {
        return true;
    }

}

测试类如下所示。

@RunWith(PowerMockRunner.class)
@PrepareForTest(SpyPrivateMethod.class)
public class PowerMockTest {

    @Test
    public void spyPrivate() throws Exception {
        SpyPrivateMethod spyPrivateMethod = PowerMockito.spy(new SpyPrivateMethod());
        PowerMockito.doReturn(false).when(spyPrivateMethod, "returnTrue");
        assertThat(spyPrivateMethod.isTrue(), is(false));
    }

}

十. Spy静态方法

1. Spy静态公共方法

被测试类如下所示。

public class SpyStaticPublicMethod {

    public static boolean isTrue() {
        return true;
    }

}

测试类如下所示。

@RunWith(PowerMockRunner.class)
@PrepareForTest(SpyStaticPublicMethod.class)
public class PowerMockTest {

    @Test
    public void spyStaticPublic() throws Exception {
        PowerMockito.spy(SpyStaticPublicMethod.class);
        PowerMockito.doReturn(false).when(SpyStaticPublicMethod.class, "isTrue");
        assertThat(SpyStaticPublicMethod.isTrue(), is(false));
    }

}

2. Spy静态私有方法

被测试类如下所示。

public class SpyStaticPrivateMethod {

    public static boolean isTrue() {
        return returnTrue();
    }

    private static boolean returnTrue() {
        return true;
    }

}

测试类如下所示。

@RunWith(PowerMockRunner.class)
@PrepareForTest(SpyStaticPrivateMethod.class)
public class PowerMockTest {

    @Test
    public void spyStaticPrivate() throws Exception {
        PowerMockito.spy(SpyStaticPrivateMethod.class);
        PowerMockito.doReturn(false).when(SpyStaticPrivateMethod.class, "returnTrue");
        assertThat(SpyStaticPrivateMethod.isTrue(), is(false));
    }

}

十一. Answer-Spy

被测试类如下所示。

public class AnswerHelp {

    public String convert(int num) {
        return StringUtils.EMPTY;
    }

}

测试类如下所示。

@RunWith(PowerMockRunner.class)
@PrepareForTest(AnswerHelp.class)
public class PowerMockTest {

    @Test
    public void answer() {
        AnswerHelp answerHelp = PowerMockito.spy(new AnswerHelp());
        Answer<String> answer = new Answer<String>() {
            @Override
            public String answer(InvocationOnMock invocation) {
                int num = (Integer) invocation.getArguments()[0];
                if (num == 0) {
                    return "zero";
                } else if (num == 1) {
                    return "one";
                }
                return StringUtils.EMPTY;
            }
        };
        PowerMockito.doAnswer(answer).when(answerHelp).convert(anyInt());
        assertThat(answerHelp.convert(0), is("zero"));
        assertThat(answerHelp.convert(1), is("one"));
    }

    @Test
    public void given_when_then_bdd() {
        AnswerHelp answerHelp = PowerMockito.spy(new AnswerHelp());
        Answer<String> answer = new Answer<String>() {
            @Override
            public String answer(InvocationOnMock invocation) {
                int num = (Integer) invocation.getArguments()[0];
                if (num == 0) {
                    return "zero";
                } else if (num == 1) {
                    return "one";
                }
                return StringUtils.EMPTY;
            }
        };
        given(answerHelp.convert(anyInt())).willAnswer(answer);
        assertThat(answerHelp.convert(0), is("zero"));
        assertThat(answerHelp.convert(1), is("one"));
    }

}

总结

合理使用PowerMock可以处理单元测试编写中的一些常见难题,在解依赖时可以帮助我们规避例如数据库,kafka等与外部存在交互的组件的影响。


大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 11 天,点击查看活动详情