本文最后更新于:2023-02-25T15:55:46+08:00
Mock
在协同开发过程中,我们经常会遇到这样的场景。我们开发了一个类,这个类的功能是依赖于其他服务的接口,或者第三方的功能模块。但是由于是协同开发,可能其他服务的接口此时还没有实现,而我们需要对实现的类进行单元测试,那可能需要等待其他服务的实现。或者是借助的第三方功能模块非常复杂,每次测试都需要比较长的时间,此时单元测试与开发之间体验就会非常割裂。
Mock就是用来解决上面的问题的。Mock意为模拟,借助Mock我们可以模拟出所需要的对象,并且可以定义这个对象的行为。Mock可以解决如下问题:
解决依赖问题:在测试一个接口或者功能模块的时候,如果这个接口或者功能模块依赖其他接口或者其他模块,且所依赖的接口或功能模块未开发完毕,那么我们可以使用Mock模拟被依赖的接口,完成目标接口的测试
模拟复杂业务的接口:依赖一个非常复杂的业务或者第三方接口,可以直接使用Mock来模拟这个复杂的业务接口,定义它能够返回正确的结果
单独测试:在对某个类进行测试的时候,可能会涉及到一些RPC的调用,数据库,缓存操作等,这些外部的依赖我们可以将其模拟Mock掉,达到单独 测试某个类的效果
前后端联调:进行前后端分离编程的时候,前端页面开发需要调用后台的接口,但是后台接口还没有开发完成,那么完全可以借助Mock来模拟后台这个接口返回想要的数据
Mockito
简介
Mocito是一个功能强大的Java测试框架。通过Mockito可以创建Mock对象,排除外部依赖对被测试类的干扰。Mocito可以模拟对象,模拟方法的返回值,模拟抛出异常等等,同时也会记录这些模拟方法的调用次数、调用顺序等,从而进行校验,断言程序是否按照我们期望的效果运行。
在Java中主流的Mock测试工具有Mockito、JMock、EasyMock。其中,Spring
Boot默认的测试框架就是Mockito。
在开始使用Mockito之前,需要介绍几个简单的相关概念:
Mock对象:在调试期间用来作为真实对象的替代,是被测试对象所依赖的对象
Mock测试:在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试
Stub:打桩,为Mock对象的方法指定返回值(可抛出异常),即人为定义Mock对象的行为
Verify:行为验证,验证指定方法调用情况(是否被调用,调用次数等)
Assert:断言,判断输出效果是否与我们期望一致
Getting Started
为了使用Mocito,我们需要在项目中引入相关依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.mockito</groupId > <artifactId > mockito-core</artifactId > <version > 4.8.0</version > <scope > test</scope > </dependency > <dependency > <groupId > org.junit.jupiter</groupId > <artifactId > junit-jupiter</artifactId > <version > 5.9.2</version > <scope > test</scope > </dependency >
由于SpringBoot默认使用Mockito作为测试框架,因此如果我们正在开发的是SpringBoot项目,那么可以通过引入下面的starter来引入依赖:
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency >
下面简单描述当前的场景,我们现在已经完成了下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 public class MyService { private OtherService otherService; public MyService (OtherService otherService) { this .otherService = otherService; } public Integer plus (Integer integer) { return 1 + otherService.complexFunc(integer); } }
可以看到在我们的Service,实现的核心方法plus
完成的是+1操作。但是它还依赖一个其他的Service,这个Service可能会进行很复杂的运算complexFunc
。现在我们需要验证的就是这个+1操作能否正确完成,而不需要验证复杂运算是否能够完成。(实际上我们根本不需要知道复杂运算的逻辑)为了测试,我们可以写如下的单元测试代码(这里使用静态引入方便后续的调用):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.mockito;import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;import static org.mockito.Mockito.*;class MyServiceTest { @Test void plus () { OtherService mock = mock(OtherService.class); when(mock.complexFunc(any())).thenReturn(1 ); MyService myService = new MyService (mock); Integer res = myService.plus(3 ); assertEquals(2 , res); } }
在单元测试代码中,我们首先将外部依赖OtherService对象Mock掉,生成了一个mock对象。之后进行stub,人为定义了复杂运算complexFunc()
的行为,也就是这一行代码:
1 when(mock.complexFunc(any())).thenReturn(1 );
这行代码的含义是,当mock对象调用到complexFunc()
方法的时候,无论接收什么样的输入即any()
,都返回1。有了这个保障,我们就可以执行需要测试的plus方法,由于无论传入任何值,复杂运算返回都是1,因此如果我们的plus方法正确,那么返回的res就必然是2,于是我们就可以这样断言,也就是代码中的最后一行。之后我们运行单元测试,就会显示测试通过。这就显示我们的plus方法中的核心+1确实是正常工作的,至于复杂逻辑,由于我们mock掉了,它能否正常运行我们是没有测试的,不过这也并不是我们需要关心的事情,我们这里需要测试的就是核心的+1能否正常运行。(这里也可以选择运行run with Coverage
),这样会同时显示测试代码覆盖率等。
更多功能
when then
在上面的demo中,我们已经展示了stub的一种方式,即使用when().thenReturn()
来确定Mock对象的方法调用返回值。实际上类似的方式还有下面一些:
thenThrow(Throwable t)
:抛出异常
thenAnswer(Answer<?> answer)
:对方法返回进行拦截处理
thenCallRealMethod()
:调用方法的真实实现
下面我们将Mock一个List对象,举例各种方法的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 final ArrayList mockList = mock(ArrayList.class); when(mockList.add("me" )).thenReturn(true ); when(mockList.get(0 )).thenThrow(new RuntimeException ()); doNothing().when(mockList).clear(); Answer<String> answer = new Answer <String>() { @Override public String answer (InvocationOnMock invocationOnMock) throws Throwable { List mock = (List) invocationOnMock.getMock(); return "mock.size result => " + mock.size(); } }; when(mockList.get(1 )).thenAnswer(answer);
如果Mock对象的某个方法没有被打桩的话,那么调用该方法会返回默认值。
在较低版本的Mockito中,是不允许对static和final方法进行stub,不过在3.4.0版本之后可以Mock静态方法和final方法等,相关依赖在mockito-inline
中。
行为与执行顺序验证
在创建了Mock对象之后,它就会记住所有的交互,于是我们就可以选择性的记录我们感兴趣的交互,包括调用了多少次Mock对象的方法,不同方法之间的执行顺序等。
行为验证:
1 2 3 4 5 6 7 8 9 10 11 12 verify(mock).complexFunc(any()); verify(mock, atLeast(2 )).complexFunc(any()); verify(mock,atMost(3 )).complexFunc(1 ); verify(mock,never()).complexFunc(2 ); verify(mock, timeout(100 )).complexFunc(1 ); verify(mock,timeout(200 ).atLeastOnce()).complexFunc(any());
执行顺序验证,需要从Mock对象中创建InOrder对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 final List mockList = mock(List.class); mockList.add("first" ); mockList.add("second" );final InOrder inOrder = inOrder(mockList); inOrder.verify(mockList).add("first" ); inOrder.verify(mockList).add("second" );final List mockList1 = mock(List.class);final List mockList2 = mock(List.class); mockList1.get(0 ); mockList1.get(1 ); mockList2.get(0 ); mockList1.get(2 ); mockList2.get(1 );final InOrder inOrder1 = inOrder(mockList1, mockList2); inOrder1.verify(mockList1).get(0 ); inOrder1.verify(mockList1).get(2 ); inOrder1.verify(mockList2).get(1 );
Spy监控
我们可以为真实的依赖对象创建一个Spy对象。这个对象与上面的Mock对象存在一定的区别。
Mock对象是对真实对象的完全Mock,我们创建的Mock对象与真实对象无关,我们可以对Mock对象的方法进行打桩,人为指定对应的返回值。而没有打桩的方法,则是默认的返回值。我们可以基于接口、实现类创建Mock对象。
Spy对象则是对真实对象的部分Mock,它是基于真实对象的。我们同样可以对对应的方法进行打桩。但是没有打桩的方法,走的是真实对象的实现。因此我们只能基于实现类来创建Spy对象,因为否则在调用没有打桩的方法时走真实实现的时候会出现异常。
1 2 3 4 5 6 7 8 9 10 LinkedList<Object> linkedList = new LinkedList <>(); LinkedList<Object> spy = spy(linkedList); spy.add("one" ); spy.add("two" ); when(spy.get(0 )).thenReturn("1_stub" ); System.out.println(spy.get(0 )); System.out.println(spy.get(1 ));
注解简化
在前面的介绍中,我们都是使用mock()
或spy()
方法来进行Mock对象或Spy对象的创建。事实上我们可以通过相关注解来管理Mock对象的创建。我们可以将外部依赖对象放在成员变量中,然后使用@Mock
或者@Spy
进行修饰,这就表示我们需要将这个对象创建为Mock对象或Spy对象。注解的使用需要首先开启,即第一步执行下面的代码:
1 MockitoAnnotations.openMocks(this );
由于每个单元测试方法是单独执行的,并且都需要开启注解,所以我们可以将注解的开启放在所有测试方法的前面,即实现一个setUp
方法,其中完成注解功能开启。以下是使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class AnnotationTest { @Mock private OtherService otherService; @BeforeEach void setUp () { MockitoAnnotations.openMocks(this ); } @Test void testMyService () { when(otherService.complexFunc(1 )).thenReturn(2 ); System.out.println(otherService.complexFunc(1 )); System.out.println(otherService.complexFunc(2 )); } }
参考文章
Mockito
(Mockito 5.1.1 API) (javadoc.io)
手把手教你Mockito的使用
- 掘金 (juejin.cn)