单元测试-PowerMock

前言

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

准备工作

一. 注解添加与使用场景

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

场景注解
模拟final方法@PrepareForTest@RunWith
模拟静态方法@PrepareForTest@RunWith
模拟私有方法@PrepareForTest
使用whenNew@PrepareForTest@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 public方法

public class Mock {

    public boolean isTrue_1() {
        return true;
    }

}

public class PowerMockTest {

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

}

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

public class Mock {

    public boolean isTrue_1() {
        return true;
    }

}

public class PowerMockTest {

    @Test
    public void mockPublicThenCallRealMethod() {
        Mock mock = PowerMockito.mock(Mock.class);
        PowerMockito.when(mock.isTrue_1()).thenCallRealMethod();
        assertThat(mock.isTrue_1(), is(true));
    }

}

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

public class Mock {

    public boolean isTrue() {
        return true;
    }

}

public class TestObj {

    public boolean isTrue() {
        Mock mock = new Mock();
        return mock.isTrue();
    }

}

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

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

}

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

二. mock final public方法

mock public方法操作一致。如下所示。

public class Mock {

    public final boolean isTure_2() {
        return true;
    }

}

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

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

}

三. mock private方法

public class Mock {

    public boolean isTrue_3() {
        return returnTrue_1();
    }
    
    private boolean returnTrue_1() {
        return true;
    }

}

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

    @Test
    public void mockPrivate() throws Exception {
        Mock mock = PowerMockito.mock(Mock.class);
        PowerMockito.when(mock, "returnTrue_1").thenReturn(false);
        PowerMockito.when(mock.isTrue_3()).thenCallRealMethod();
        assertThat(mock.isTrue_3(), is(false));
    }

}

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

public class Mock {
    
    private boolean returnTrue_1() {
        return true;
    }

}

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

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

}

四. mock static public方法

通常是针对工具类进行mock。如下所示。

public class Mock {
    
    public static boolean isTrue_4() {
        return true;
    }

}

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

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

}

五. mock static private方法

public class Mock {
    
    public static boolean isTrue_5() {
        return returnTrue_2();
    }
    
    private static boolean returnTrue_2() {
        return true;
    }

}

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

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

}

同样也可以使用Whitebox来方便的调用静态私有方法,如下所示。

public class Mock {
    
    private static boolean returnTrue_2() {
        return true;
    }

}

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

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

}

六. Whitebox

使用Whitebox可以方便的设置对象(静态)私有属性值,如下所示。

public class Mock {

    private boolean flag = true;
    
    public boolean isTrue_6() {
        return flag;
    }

}

public class PowerMockTest {

    @Test
    public void whiteboxPrivateField() {
        Mock mock = new Mock();
        Whitebox.setInternalState(mock, "flag", false);
        assertThat(mock.isTrue_6(), is(false));
    }

}

仅使用Whitebox时不需要添加@RunWith@PrepareForTest注解,同时对于上面例子如果flag是静态变量,那么设置静态变量值时需要使用Whitebox.setInternalState(Mock.class, "flag", false)。使用Whitebox也可以方便的调用对象(静态)私有方法,如下所示。

public class Mock {

    private boolean isTrue_7() {
        return true;
    }

}

public class PowerMockTest {

    @Test
    public void whiteboxPrivateMethod() throws Exception {
        Mock mock = new Mock();
        assertThat(Whitebox.invokeMethod(mock, "isTrue_7"), is(true));
    }

}

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

七. Answer-mock

针对同一方法多次被调用且不同入参需要mock不同出参的情况,可以使用Answer,如下所示。

public class Mock {

    public String isTrue_8(int num) {
        return "";
    }

}

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

    @Test
    public void answer() {
        Mock mock = PowerMockito.mock(Mock.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 null;
            }
        };
        PowerMockito.when(mock.isTrue_8(anyInt())).thenAnswer(answer);
        assertThat(mock.isTrue_8(0), is("zero"));
        assertThat(mock.isTrue_8(1), is("one"));
    }

}

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

public class Mock {

    public String isTrue_8(int num) {
        return "";
    }

}

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

    @Test
    public void answer() {
        Mock mock = PowerMockito.mock(Mock.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 null;
            }
        };
        given(mock.isTrue_8(anyInt())).willAnswer(answer);
        assertThat(mock.isTrue_8(0), is("zero"));
        assertThat(mock.isTrue_8(1), is("one"));
    }

}

八. spy public方法

public class Spy {

    public boolean isTrue_1() {
        return true;
    }
    
    public boolean isTrue_2() {
        return true;
    }

}

public class SpyTest() {

    @Test
    public void spyPublic() {
        Spy spy = PowerMockito.spy(new Spy());
        PowerMockito.doReturn(false).when(spy).isTrue_1();
        assertThat(spy.isTrue_1(), is(false));
        assertThat(spy.isTrue_2(), is(true));
    }

}

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

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

使用场景doReturn()thenReturn()
打桩public方法PowerMockito.doReturn(false).when(spy).isTrue_1()PowerMockito.when(spy.isTrue_1()).thenReturn(false)
打桩private方法PowerMockito.doReturn(false).when(spy, "returnTrue_1")PowerMockito.when(spy, "returnTrue_1").thenReturn(false)
打桩static方法PowerMockito.doReturn(false).when(Spy.class, "isTrue_5")PowerMockito.when(Spy.isTrue_5()).thenReturn(false)
打桩static private方法PowerMockito.doReturn(false).when(Spy.class, "returnTrue") PowerMockito.when(Spy.class, "returnTrue").thenReturn(false)

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

九. spy private方法

public class Spy {

    public boolean isTrue_4() {
        return returnTrue_1();
    }
    
    private boolean returnTrue_1() {
        return true;
    }

}

@RunWith(PowerMockRunner.class)
@PrepareForTest(Spy.class)
public class SpyTest() {

    @Test
    public void spyPrivate() throws Exception {
        Spy spy = PowerMockito.spy(new Spy());
        PowerMockito.doReturn(false).when(spy, "returnTrue_1");
        assertThat(spy.isTrue_4(), is(false));
    }

}

十. spy static public方法

public class Spy {

    public static boolean isTrue_5() {
        return true;
    }

}

@RunWith(PowerMockRunner.class)
@PrepareForTest(Spy.class)
public class SpyTest() {

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

}

十一. spy static private方法

public class Spy {

    public static boolean isTrue_6() {
        return returnTrue();
    }
    
    private static boolean returnTrue_2() {
        return true;
    }

}

@RunWith(PowerMockRunner.class)
@PrepareForTest(Spy.class)
public class SpyTest() {

    @Test
    public void spyStaticPrivate() throws Exception {
        PowerMockito.spy(Spy.class);
        PowerMockito.doReturn(false).when(Spy.class, "returnTrue_2");
        assertThat(Spy.isTrue_6(), is(false));
    }

}

十二. Answer-spy

public class Spy {

    public String isTrue_7(int num) {
        return "";
    }

}

@RunWith(PowerMockRunner.class)
@PrepareForTest(Spy.class)
public class SpyTest() {

    @Test
    public void answer() {
        Spy spy = PowerMockito.spy(new Spy());
        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 "";
            }
        };
        PowerMockito.doAnswer(answer).when(spy).isTrue_7(anyInt());
        assertThat(spy.isTrue_7(0), is("zero"));
        assertThat(spy.isTrue_7(1), is("one"));
    }
    
    @Test
    public void given_when_then_bdd() {
        Spy spy = PowerMockito.spy(new Spy());
        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 "";
            }
        };
        given(spy.isTrue_7(anyInt())).willAnswer(answer);
        assertThat(spy.isTrue_7(0), is("zero"));
        assertThat(spy.isTrue_7(1), is("one"));
    }

}

总结

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

41 声望
23 粉丝
0 条评论
推荐阅读
数据库连接池-Druid数据库连接池源码解析
本文将对Druid数据库连接池的源码进行分析和学习,以了解Druid数据库连接池的工作原理。Druid数据库连接池的基本逻辑几乎全部在DruidDataSource类中,所以本文主要是围绕DruidDataSource的各项功能展开论述。

半夏之沫阅读 1k

刨根问底 Redis, 面试过程真好使
充满寒气的互联网如何在面试中脱颖而出,平时积累很重要,八股文更不能少!下面带来的这篇 Redis 问答希望能够在你的 offer 上增添一把🔥。

菜农曰17阅读 957

封面图
PHP转Go实践:xjson解析神器「开源工具集」
我和劲仔都是PHP转Go,身边越来越多做PHP的朋友也逐渐在用Go进行重构,重构过程中,会发现php的json解析操作(系列化与反序列化)是真的香,弱类型语言的各种隐式类型转换,很大程度的减低了程序的复杂度。

王中阳Go10阅读 2k评论 3

封面图
万字详解,吃透 MongoDB!
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常...

JavaGuide5阅读 827

封面图
计算机网络连环炮40问
本文已经收录到Github仓库,该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点,欢迎star~

程序员大彬8阅读 1.1k

与RabbitMQ有关的一些知识
工作中用过一段时间的Kafka,不过主要还是RabbitMQ用的多一些。今天主要来讲讲与RabbitMQ相关的一些知识。一些基本概念,以及实际使用场景及一些注意事项。

lpe2348阅读 1.9k

封面图
Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 2.3k评论 2

封面图
41 声望
23 粉丝
宣传栏