单元测试

计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。(摘自维基百科)

什么是Mock

在面向对象程序设计中,模拟对象(mock object)是以可控的方式模拟真实对象行为的假的对象。

比如,对象A依赖对象B,但是B代码还没具体实现,还是一个接口,不能用,我们可以mock一个假的B来完成测试;

又比如,开发过程中,服务分开部署,在本地开发环境无法调用依赖服务接口或是调用过程复杂,我们也可以mock一个被调用对象来完成单元测试。

为什么要用Mock

  • 真实对象的行为是不确定的(例如当前的时间);
  • 真实对象很难搭建起来;
  • 真实对象的行为很难触发(例如网络错误);
  • 真实对象速度很慢(例如项目很大,启动缓慢);

使用Mock的好处

  • Mock可以用来解除外部服务依赖,从而保证了测试用例的独立性

    现在的互联网软件系统,通常采用了分布式部署的微服务,为了单元测试某一服务而准备其它服务,存在极大的依耐性和不可行性。

  • Mock可以减少全链路测试数据准备,从而提高了编写测试用例的速度

    传统的集成测试,需要准备全链路的测试数据,可能某些环节并不是你所熟悉的。最后,耗费了大量的时间和经历,并不一定得到你想要的结果。现在的单元测试,只需要模拟上游的输入数据,并验证给下游的输出数据,编写测试用例并进行测试的速度可以提高很多倍。

  • Mock可以模拟一些非正常的流程,从而保证了测试用例的代码覆盖率

    根据单元测试的BCDE原则,需要进行边界值测试(Border)和强制错误信息输入(Error),这样有助于覆盖整个代码逻辑。在实际系统中,很难去构造这些边界值,也能难去触发这些错误信息。而Mock从根本上解决了这个问题:想要什么样的边界值,只需要进行Mock;想要什么样的错误信息,也只需要进行Mock

    补充:B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
    C: Correct,正确的输入,并得到预期的结果。
    D:Design,与设计文档相结合,来编写单元测试。
    E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。

  • Mock可以不用加载项目环境配置,从而保证了测试用例的执行速度

    在进行集成测试时,我们需要加载项目的所有环境配置,启动项目依赖的所有服务接口。往往执行一个测试用例,需要几分钟乃至几十分钟。采用Mock实现的测试用例,不用加载项目环境配置,也不依赖其它服务接口,执行速度往往在几秒之内,大大地提高了单元测试的执行速度。

Mockito介绍

MockitoGitHub上使用最广泛的Mock框架,并与JUnit结合使用,Mockito框架可以创建和配置mock对象,使用Mockito简化了具有外部依赖的类的测试开发。

GitHub地址:https://github.com/mockito/mockito

PowerMock介绍

PowerMock是对Mockito的扩展,PowerMock可以实现完成对privatestaticfina方法的Mock(模拟),而Mockito可以对普通的方法进行Mock,如:public等。

GitHub地址:https://github.com/powermock/powermock

SpringBoot中使用PowerMock

Maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>

开始使用

简单的测试用例 -> com.example.mock.other.ListTest#testSize;

Mock方法

  • 声明:

    1
    T PowerMockito.mock(Class clazz);
  • 用途:可以用于模拟指定类的对象实例。

    当模拟非final类(接口、普通类、虚基类)的非final方法时,不必使用@RunWith@PrepareForTest注解。当模拟final类或final方法时,必须使用@RunWith@PrepareForTest注解。注解形如:

    1
    2
    @RunWith(PowerMockRunner.class)
    @PrepareForTest({TargetClass.class})
  • 模拟普通方法

    com.example.mock.service.ArticleServiceTest#testGetLikesById

  • 模拟final类或final方法

    com.example.mock.other.CircleTest#testGetArea

Mock静态方法

  • 声明

    1
    PowerMockito.mockStatic(Class clazz);
  • 用途:可以用于模拟类的静态方法,必须使用@RunWith@PrepareForTest注解。

  • 模拟静态方法

    com.example.mock.other.StringsTest#testIsEmpty

spy语句

如果一个对象,我们只希望模拟它的部分方法,而希望其它方法跟原来一样,可以使用PowerMockito.spy方法代替PowerMockito.mock方法。于是,通过when语句设置过的方法,调用的是模拟方法;而没有通过when语句设置的方法,调用的是原有方法。

spy

  • 声明

    1
    PowerMockito.spy(Class clazz);
  • 用途:用于模拟类的部分方法。

  • 模拟部分方法

    com.example.mock.other.StringsTest#testIsNotEmpty

when语句之when().thenReturn()模式

  • 声明

    1
    2
    3
    4
    5
    6
    7
    PowerMockito.when(mockObject.someMethod(someArgs)).thenReturn(expectedValue);

    PowerMockito.when(mockObject.someMethod(someArgs)).thenThrow(expectedThrowable);

    PowerMockito.when(mockObject.someMethod(someArgs)).thenAnswer(expectedAnswer);

    PowerMockito.when(mockObject.someMethod(someArgs)).thenCallRealMethod();
  • 用途:用于模拟对象方法,先执行原始方法,再返回期望的值、异常、应答,或调用真实的方法。

  • 返回期望值

    com.example.mock.other.ListTest#testGet

  • 返回期望异常

    com.example.mock.other.ListTest#testGetException

  • 返回期望应答

    com.example.mock.other.ListTest#testGetAnswer

  • 调用真实方法

    com.example.mock.other.ListTest#testGetCallRealMethod

when语句之doReturn().when()模式

  • 声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    PowerMockito.doReturn(expectedValue).when(mockObject).someMethod(someArgs);

    PowerMockito.doThrow(expectedThrowable).when(mockObject).someMethod(someArgs);

    PowerMockito.doAnswer(expectedAnswer).when(mockObject).someMethod(someArgs);

    PowerMockito.doNothing().when(mockObject).someMethod(someArgs);

    PowerMockito.doCallRealMethod().when(mockObject).someMethod(someArgs);
  • 用途:用于模拟对象方法,直接返回期望的值、异常、应答,或调用真实的方法,无需执行原始方法。

  • 禁用语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    PowerMockito.doReturn(expectedValue).when(mockObject.someMethod(someArgs));

    PowerMockito.doThrow(expectedThrowable).when(mockObject.someMethod(someArgs));

    PowerMockito.doAnswer(expectedAnswer).when(mockObject.someMethod(someArgs));

    PowerMockito.doNothing().when(mockObject.someMethod(someArgs));

    PowerMockito.doCallRealMethod().when(mockObject.someMethod(someArgs));

    虽然不会出现编译错误,但是在执行时会抛出UnfinishedStubbingException异常。

    com.example.mock.other.StringsTest#testUnfinishedStubbingException

  • 返回期望值

    com.example.mock.other.ListTest#testGetDoReturn

  • 返回期望异常

    com.example.mock.other.ListTest#testGetIndexOutOfBoundsException

  • 返回期望应答

    com.example.mock.other.ListTest#testGetDoAnswer

  • 模拟无返回值

    com.example.mock.other.ListTest#testGetDoNothing

  • 调用真实方法

    com.example.mock.other.ListTest#testGetDoCallRealMethod

两种模式的主要区别

​ 两种模式都用于模拟对象方法,在mock实例下使用时,基本上是没有差别的。但是,在spy实例下使用时, when().thenReturn()模式会执行原方法,而doReturn().when()模式不会执行原方法。

使用when().thenReturn()模式

com.example.mock.service.ArticleServiceTest#testGetLikesByIdWhenReturn

​ 会打印出真实方法中的日志。

使用doReturn().when()模式

com.example.mock.service.ArticleServiceTest#testGetLikesByIdDoReturnWhen

​ 不会打印出真实方法中的日志信息。

whenNew模拟构造方法

  • 声明

    1
    2
    3
    PowerMockito.whenNew(MockClass.class).withNoArguments().thenReturn(expectedObject);

    PowerMockito.whenNew(MockClass.class).withArguments(someArgs).thenReturn(expectedObject);
  • 用途:用于模拟构造方法。

    com.example.mock.other.FileUtilsTest#testIsFile

    注意:因为FileUtilsfinal类,需要加上注解@PrepareForTest({FileUtils.class}),否则模拟方法不生效。

参数匹配器

在执行单元测试时,有时候并不关心传入的参数的值,可以使用参数匹配器。

参数匹配器(any)

Mockito提供Mockito.anyInt()Mockito.anyString()Mockito.any(Class<T> clazz)等来表示任意值。

com.example.mock.other.ListTest#testGetAnyInt

参数匹配器(eq)

当我们使用参数匹配器时,所有参数都应使用匹配器。如果要为某一参数指定特定值时,就需要使用Mockito.eq()方法。

com.example.mock.other.StringsTest#testStartWith

附加匹配器

MockitoAdditionalMatchers类提供了一些很少使用的参数匹配器,我们可以进行参数大于(gt)、小于(lt)、大于等于(geq)、小于等于(leq)等比较操作,也可以进行参数与(and)、或(or)、非(not)等逻辑计算等。

com.example.mock.other.ListTest#testAdditionalMatchers

verify语句

验证是确认在模拟过程中,被测试方法是否已按预期方式与其任何依赖方法进行了交互。

  • 格式

    1
    Mockito.verify(mockObject[,times(int)]).someMethod(somgArgs);
  • 用途:用于模拟对象方法,直接返回期望的值、异常、应答,或调用真实的方法,无需执行原始方法。

  • 验证调用方法

    com.example.mock.other.ListTest#testVerifyMethod

  • 验证调用次数

    com.example.mock.other.ListTest#testCallNum

    times外,Mockito还支持atLeastOnce(至少一次)、atLeast(至少n次)、only(仅执行某方法)、atMostOnce(最多一次)、atMost(最多n次)等次数验证器。

  • 验证调用顺序

    com.example.mock.other.ListTest#testAddOrder

  • 确保验证完毕

    Mockito提供Mockito.verifyNoMoreInteractions方法,在所有验证方法之后可以使用此方法,以确保所有调用都得到验证。如果模拟对象上存在任何未验证的调用,将会抛出NoInteractionsWanted异常。

    备注:Mockito.verifyZeroInteractions方法与Mockito.verifyNoMoreInteractions方法相同,但是目前已经被废弃。

    com.example.mock.other.ListTest#testVerifyNoMoreInteractions

  • 验证静态方法

    Mockito没有静态方法的验证方法,但是PowerMock提供这方面的支持。

    com.example.mock.other.StringsTest#testVerifyStatic

私有属性

Whitebox.setInternalState方法

com.example.mock.service.ArticleServiceTest#testGetUserLimit

​ 备注:需要加上注解@RunWith(PowerMockRunner.class)

主要注解

PowerMock为了更好地支持SpringMVC/SpringBoot项目,提供了一系列的注解,大大地简化了测试代码。

  • @RunWith注解

@RunWith(PowerMockRunner.class)

指定JUnit 使用 PowerMock 框架中的单元测试运行器。

  • @PrepareForTest注解

    @PrepareForTest({ TargetClass.class })

    当需要模拟final类、final方法或静态方法时,需要添加@PrepareForTest注解,并指定方法所在的类。如果需要指定多个类,在{}中添加多个类并用逗号隔开即可。

  • @Mock注解

    @Mock注解创建了一个全部Mock的实例,所有属性和方法全被置空(0或者null)。

  • @Spy注解

    @Spy注解创建了一个没有Mock的实例,所有成员方法都会按照原方法的逻辑执行,直到被Mock返回某个具体的值为止。

    注意:@Spy注解的变量需要被初始化,否则执行时会抛出异常。

  • @InjectMocks注解

    @InjectMocks注解创建一个实例,这个实例可以调用真实代码的方法,其余用@Mock或@Spy注解创建的实例将被注入到用该实例中。

  • @Captor注解

    @Captor注解在字段级别创建参数捕获器。但是,在测试方法启动前,必须调用MockitoAnnotations.openMocks(this)进行初始化。

  • @PowerMockIgnore注解

    为了解决使用PowerMock后,提示ClassLoader错误。

单元测试与集成测试的区别

在实际工作中,不少同学用集成测试代替了单元测试,或者认为集成测试就是单元测试。这里,总结为了单元测试与集成测试的区别。

  • 测试对象不同

    单元测试对象是实现了具体功能的程序单元,集成测试对象是概要设计规划中的模块及模块间的组合。

  • 测试方法不同

    单元测试中的主要方法是基于代码的白盒测试,集成测试中主要使用基于功能的黑盒测试。

  • 测试时间不同

    集成测试要晚于单元测试。

  • 测试内容不同

    单元测试主要是模块内程序的逻辑、功能、参数传递、变量引用、出错处理及需求和设计中具体要求方面的测试;而集成测试主要验证各个接口、接口之间的数据传递关系,及模块组合后能否达到预期效果。

项目GitHub地址

项目GitHub地址: https://github.com/feihongxiansen/springboot-demo-powermock.git