单元测试之带你搞懂Mockito使用

Mock介绍

在平时开发过程中,我们往往会遇到以下问题
1.由于依赖调用的接口没有开发完成,需要等待(客户端和服务端,服务端和其他服务之间)
2.自测时由于服务器故障等无法正常调用接口,或者一些边界条件无法在测试环境模拟数据
3.同样的单元测试,当依赖的数据发生变化时,无法反复执行,不能在上线前对之前的功能进行自动回归

mock就帮我们解决了以上问题

mock的定义(what)
mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为

哪些时机和场合需要使用mock(when&where):
1.单元测试/接口测试中测试对象依赖其他对象,这些对象的构造复杂、耗时或者根本无法构造(未交付)
2.我们只测试对象内部逻辑的质量,不关心依赖对象的逻辑正确性和稳定性
3.一些边界条件无法在正常情形下模拟 比方说接口异常返回

使用mock的时候并不需要对所有的依赖服务(对象)都进行Mock,只需要对不容易构造的对象或者不稳定的对象进行mock可以了。当然,mock的前提是底层服务提供的数据是值得信任的,实际开发过程还是需要进行联调测试的。

像EasyMock , Mockito , PowerMock都是常用的mock框架

powerMock是基于easyMock或Mockito扩展出来的增强版本,能够mock静态、final、私有方法等,这些都是EasyMock和Mockito不能做到的。

Mockito和EasyMock的功能差不多,但是Mockito不需要录制、播放这些动作,语法上比EasyMock更灵活,可读性更好
个人比较喜欢Mockito和PowerMock

今天就先来简单讲讲关于Mockito的使用,之后会单独再讲下PowerMock的增强点,就是对于静态、final、私有方法的mock

Mockito使用

1.Maven项目,需要先引入如下pom
<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-all</artifactId>
   <version>1.9.5</version>
   <scope>test</scope>
</dependency>

你也可以使用mockito-core的pom,不过mockito-core里只包含mockito类,而mockito-all会包含mockito类以及一些依赖项,比方说hamcrest。
实际上mockito-all已经停止更新,Mockito 2中已停止使用“mockito-all”发行版。*。

这里我使用的是mockito-all,两个pom创建代理对象的方式不一样,Mockito1是通过CGlib,而Mockito2是使用的ByteBuddy,常用的api都没有区别,我后面提到的用法两个pom都支持

2.@Mock和@InjectMocks注解

@Mock : 为某个类创建Mock对象,使用方式如下(通常直接加在属性字段上):

    @Mock
    private GetAirportTransferOrderHandler getAirportTransferOrderHandler;

等价于

 GetAirportTransferOrderHandler singleMock =  Mockito.mock(GetAirportTransferOrderHandler.class);

@InjectMocks

@InjectMocks - injects mock or spy fields into tested object automatically.

也就是说为被测试对象自动注入mock或者spy的字段 (有点类似于spring的依赖注入的感觉),注入的方式有三种:构造方法注入,setter注入,属性注入

3.Mockito初始化的方式

单元测试之带你搞懂Mockito使用_第1张图片
官方文档上的意思就是如果使用了@Mock和@InjectMocks注解,那么我们就需要调用MockitoAnnotations.initMocks(testClass)来帮助我们完成mock对象的创建和自动注入

Mockito初始化的方式有三种:

//方式一
@Before
public void init() {
   MockitoAnnotations.initMocks(this);
}
//方式二
@RunWith(MockitoJUnitRunner.class)
//方式三
@Rule
public MockitoRule mockito = MockitoJUnit.rule()

三者的使用效果都是一样的,主要是为了
1.提供mock初始化工作
2.为unit test提供框架使用的自动验证

使用方式一或者方式三的好处在于不需要使用Mockito的Runner,就可以使用其他的Runner了,比方说SpringJUnit4ClassRunner

4.Stubbing 插桩

设置mock对象的某个方法返回期望值
when(mockedObject.method() ).thenReturn( expectValue);

对于没有stub过的有返回值的方法,会返回默认值(0,false,null等)

@RunWith(MockitoJUnitRunner.class)
public class GetAirportTransferOrderListCiTest {

    @InjectMocks
    private GetAirportTransferOrderListProcessor getAirportTransferOrderListProcessor;

    @Mock
    private GetAirportTransferOrderHandler getAirportTransferOrderHandler;

    @Test
    public void testGetAirportTransferOrderList() throws Throwable {

        GetAirportTransferOrderListRequestType getAirportTransferOrderListRequestType = new GetAirportTransferOrderListRequestType();
        getAirportTransferOrderListRequestType.setUid("test");
        getAirportTransferOrderListRequestType.setLocale("zh-HK");
        getAirportTransferOrderListRequestType.setOrderIds(Lists.newArrayList(1212));
        when(getAirportTransferOrderHandler.handle(anyObject())).thenReturn(getAirportTransferOrderList());
        GetAirportTransferOrderListResponseType responseType = getAirportTransferOrderListProcessor.execute(getAirportTransferOrderListRequestType);
        Assert.assertTrue(responseType.getOrderInfos().size() == 1);
       
    }

可以调用方法时模拟抛出异常,适合于模拟第三方接口或者服务故障情形
when(RedisManager.getInstance()).thenThrow(new Exception(“error get redis connection”));

对于没有返回值的方法,可以通过如下方式来进行插桩
Mockito.doNothing().when(mockObject).voidMethod(param);
也可以使用doAnswer来插桩

        doAnswer(new Answer() {
            public Object answer(InvocationOnMock invocation) {
                String username = (String) invocation.getArguments()[0];
                System.out.println(username);
                return  null;
            }
        }).when(userService).delete("a","a");//这里的delete是一个没有返回值的方法
        userService.delete("a","a");

Answer由于可以获取到方法调用的参数信息,使用doAnswer我们还可以根据方法的调用参数返回不同的结果

doAnswer(new Answer() {
            public Object answer(InvocationOnMock invocation) {
                String username = (String) invocation.getArguments()[0];
                if(!StringUtils.isEmpty(username)){
                    return "有姓名";
                }
                return  "没有姓名";
            }
        }).when(userService).show(anyString(),anyString());
5.Argument Matchers 参数匹配

Mockito的参数匹配有两种方式:
1.传入实际的参数
判断是否匹配的时候直接通过equals方法进行比较

when(commonService.getCityName(2,“en-US”)).thenReturn(“shanghai”);

2.使用arguments matchers对象

eg.anyInt(),anyString(),anyObject(),anySet(),eq()等

下面的参数匹配方式就是说明,对于调用getAirportTransferOrderHandler的handle方法,无论是什么参数都返回getAirportTransferOrderList()的数据结果

when(getAirportTransferOrderHandler.handle(anyObject())).thenReturn(getAirportTransferOrderList());

注意上述两种参数匹配方式是不能混用的
when(commonService.getCityName(eq(2),“en-US”)).thenReturn(“shanghai”); 错误
when(commonService.getCityName(eq(2),eq(“en-US”)).thenReturn(“shanghai”); 正确
when(commonService.getCityName(eq(2),anyString()).thenReturn(“shanghai”); 正确

如果我们需要自己定义参数的验证规则,Mockito还提供了custom argument matchers – argThat
使用如下:

 when(updateDataHandler.handle(argThat(new ArgumentMatcher<UpdateDataRequestType>() {
            @Override
            public boolean matches(Object o) {
                //可以定义自己想要的任何匹配逻辑
                UpdateDataRequestType request = (UpdateDataRequestType) o;
                if (!request.getType().equals("CP")){
                    return false ;
                }
                return true;
            }
        }))).thenReturn(response);
6.verify 验证mock方法被调用了特定次数/至少x次/最多x次/从未被调用
//是否add("twice")被调用了两次。
verify(mockedList, times(2)).add("twice");
//验证add("twice")被调用了至少一次 等价于verify(mockedList).add("twice");
verify(mockedList, atLeastOnce()).add("twice");
verify(mockedList, atLeast(2)).add("twice");
verify(mockedList, atMost(5)).add("twice");
verify(mockedList, never()).add("twice");

我在开发过程中有时会使用verify来验证缓存是否生效,调用两次代码,如果mock只被执行了一次,说明第二次缓存命中

7.Spy

You can create spies of real objects. When you use the spy then the real methods are called (unless a method was stubbed).

关于spy官方的定义写的很清楚,spy是用于创建对应的真实对象,如果对应的方法我们自己不主动进行插桩的话就会执行对应的真实业务代码

对于Spy的对象,他调用的都是真实方法,如果你想让他返回的值按照自己设定的来,就需要自己去mock
适合于对象内有大量的调用了大量的方法,但实际只需要mock关注的少量方法即可

看下我实际使用的一个例子:

使用@Mock生成的类,默认所有方法都不是真实的方法,而且返回值都是NULL。
使用@Spy生成的类,默认所有方法都是真实方法,返回值都是和真实方法一样的。

当用when去设置mock返回值时,它里面的方法(cache.get(XX))会先执行一次。使用doReturn去设置的话,就不会产生上面的问题

所以如果需要对Spy的对象进行mock的时候,推荐都直接使用doReturn或者doThrow的句式,否则就会先调用一次真实的业务逻辑。如果你的数据有些初始化操作没有执行,那么就可能出现异常

像下面这种情形,由于spy的List并没有添加任何元素,直接执行spy.get(0)就会发生数组越界问题


   List list = new LinkedList();
   List spy = spy(list);
   //Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
   when(spy.get(0)).thenReturn("foo");
   //You have to use doReturn() for stubbing
   doReturn("foo").when(spy).get(0);

注意:由于@Spy是监控的真实对象,我们需要先得到一个真实的对象,才可以对它使用spy的功能。一般可以通过spring容器帮我们生成bean或者自己手动new创建对象

8.同一个mock多次调用相同方法返回不同的结果
Mockito.when(methodCall).thenReturn(result1).thenReturn(result2).thenReturn(resultx)
//简化写法
Mockito.when(methodCall).thenReturn(result1,result2,resultx)

这个适合于需要mock迭代器的场景,或者同样的方法在执行过程中需要多次调用,当然对于这种情形是可以通过不同的参数匹配和doAnswer来解决的

9.链式调用的mock

一般情形下我们都是创建一个mock对象,然后对其进行插桩,但是有的的情形下我们可能需要mock一个链式调用的对象,比方说建造者模式下的Builder
我们可以这样写

public class User {

    private String username;
    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String show() {
        return "username:"+ username+",password:"+password;
    }
}
public class UserService {
  
    public String  show(String name  ,String a) {
        return "1";
    }

    public void  delete(String username, String password) {
        System.out.println("删除成功....");
    }

    public  User getUser(){
        return new User("aa","bb");
    }
}
        UserService userService = Mockito.mock(UserService.class);
        User user = Mockito.mock(User.class);
        Mockito.when(userService.getUser()).thenReturn(user);
        Mockito.when(user.show()).thenReturn("mock show");
        Assert.assertEquals("mock show",userService.getUser().show());

对于一两次的链式调用还好,如果次数多了,mock起来就会比较麻烦,Mockito提供了Deep Stub的功能
上面的写法等价于下面的


 UserService userService= Mockito.mock(UserService.class,RETURNS_DEEP_STUBS);
 Mockito.when(userService.getUser().show()).thenReturn("mock test");
 Assert.assertEquals("mock test",userService.getUser().show());

参考文档:
https://javadoc.io/doc/org.mockito/mockito-core/2.26.0/org/mockito/Mockito.html
翻译版:
Mockito 中文文档 ( 2.0.26 beta )

你可能感兴趣的:(单元测试,单元测试,mockito)