Mockito

泪湿孤枕 提交于 2020-01-08 19:36:04

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

前言

在多个成员协同开发的时候,有没有遇到过类似这样的情况:

A成员在开发某个接口但是没有完成,而B恰好依赖于A,在不了解Mockito的情况下只能等待A完成之后才可以自测,完全受制于他人,不知你是否想过怎么摆脱这样一个尴尬的场景,下面咱们就根据这样一个场景来做具体讲述。

目录

  1. 什么是Mockito?
  2. 为什么需要模拟?
  3. 有哪些需要注意的关键点?
  4. 如何Mock?
  5. 结合Junit使用
  6. 注意事项

正文

1. 什么是Mockito?

Mockito是一个单元测试框架,封装了简洁易用的API,对于新手来说也可以很容易的入门,同时在我们编写单元测试的时候还可以以锻炼我们的逻辑思维能力。

2. 为什么需要模拟?

原因一: 在我们实际程序开发中经常遇到下图这种类与类之间的层级依赖:

正如前言所说,假设ImageService还未开发完毕,此时我们的开发将受限于他人

原因二: 在我们写单元测试的时候,业务层的数据大多数人都是从数据库读取,假设某位同事把数据不小心删除了,那我们的单元测试就有可能出问题。专业的说法就是:数据与应用隔离

原因三: 锻炼我们的逻辑思维能力,为什么这么说呢?在我们写Mock单元测试的时候,你必须了解你的逻辑,对你的逻辑做一些验证,比如说测试A服务的时候调用B,需要在调用B服务的时候做参数抓取校验,参数的值必须是XX,调用完毕的结果必须是XX等等;这些都是为了验证我们逻辑的正确性,从而提高我们的逻辑思维能力。

3. 哪些需要注意的关键点?

  1. mock数据:在测试某个服务的时候,需要提前把这个依赖的接口mock 数据,一般都是使用注解:@mock 、 @InjectMocks
  2. 设置预期:由于依赖其他小伙伴的接口,所以我们可以在调用接口的时候设置预期的参数以及返回值,在程序执行的时候mockito自动就会帮我们返回预计设置的值(注意这里不会真正调用依赖的接口)。当然这个只是其中一种设置预期,比如说我们还可以设置某个方法调用的次数等信息。
  3. 验证结果:一般来说设置预期与设置验证结果都是一起的。如果在一个单元测试中,如果你设定的预期没有得到满足,那么这个单元测试就是失败的。例如设置预期结果是调用userService.save的时候用户名必须走风控,头像必须上传到第三方服务器上,但是在测试中它并没有被调用,那么测试就失败了。

4. 如何Mock?

  • 4.1 模拟对象

我们创建mock依赖对象一般都是使用注解:@mock 、 @InjectMocks ,那这两个有什么区别呢?

@mock: 由Mockito框架根据接口的实现帮我们mock一个虚假对象,该对象的方法不会真正去执行

@InjectMocks: 由Mockito框架根据接口的实现帮我们创建一个实例对象(注意是实例对象,所以调用该类的方法都会被执行),同时会根据@Mock的类与@InjectMocks标识的类中的属性类型进行匹配,如果完全一致,自动注入属性值,类似于Spring的IOC功能,之前不知道,还是自己手写的工具类反射赋值的【蠢哭】

@Mock
private ImageSercice imageSercice;
@Mock
private RiskSercice riskSercice;
@InjectMocks
private UserSercice userSercice;
  • 4.2 设置预期

首先介绍下我们常用的静态方法:

4.2.1 Mockito.mock: 创建一个模拟类实例对象,与@mock的作用是一样的,注意不是@InjectMocks,想要注入依赖对象还是老老实实的使用@InjectMocks吧。当我们使用这个方法创建出来的对象,结合when().thenReturn()时候,调用的方法是不会执行的。

4.2.2 Mockito.when: 用于调用某个方法的时候设置期望,具体请看如下DEMO,设置了当调用imageService.upload(参数)时返回true,调用riskService.risk(参数)时返回new HashMap():

    @Test
    public void testWhen() {
        Mockito.when(imageService.upload(Mockito.any())).thenReturn(true);
        Mockito.when(riskService.risk(Mockito.anyMap())).thenReturn(new HashMap());
        userService.save(new HashMap<>());
    }

4.2.3 Mockito.spy::与mock类似,但他创建的是实例对象,所以调用其方法会被真实执行的,结合when().thenReturn()时候看下图DEMO所示:

4.2.4 Mockito.doThrow: 校验方法出现异常时的校验,

// 保存用户的时候抛出Exception异常
Mockito.doThrow(new NullPointerException()).when(Mockito.mock(UserService.class)).save(null);

4.2.5 Mockito.eq: 一般在设置Mockito.when参数值时使用,判断参数等于预期值,如果不相等则设置的 when 无效,如果不写的话,默认就是eq;但是如果一个方法有多个参数且不同类型,做参数值判断的时候Mockito.eq 必须写,否则运行值会报错


Mockito.when(userService.getById(1)).thenReturn(userModel); 
// 当单个参数是,可以改写成 
Mockito.when(userService.getById(Mockito.eq(1))).thenReturn(userModel); 

// 多个参数,校验的Mockito参数类型参数不同,此种情况每个参数都必须添加特定类型参数值校验,
// 示例demo:userService.get(age,name,date)
// 错误写法
Mockito.when(userService.get(1,null,new Mockito.isA(Date.class))).thenReturn(userModel);
// 正确写法
Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.isA(Date.class))).thenReturn(userModel);
// 也是正确写法,但是new Date 与程序中的new Date是两个对象,不会命中预设值的Mockito.when(userService.get(1,null,new Date())).thenReturn(userModel); 
Mockito.when(userService.get(1,null,new Date())).thenReturn(userModel);

4.2.6 Mockito.any: 返回一个对象,支持Null,一般是在mockito.when() 被调用的方法中使用,表示忽略对象值

// 与 Mockito.eq 中的demo类似 ,只是将Mockito.isA 换成了any
Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.any(Date.class))).thenReturn(userModel);

4.2.7 Mockito.isNull: 返回一个空对象,一般是在mockito.when() 被调用的方法中使用

// 与 Mockito.any 中的demo类似 
Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.any(Date.class))).thenReturn(userModel);

4.2.8 Mockito.isA: 返回一个指定类型对象与any 类似

// 与 Mockito.any 中的demo类似 
Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.any(Date.class))).thenReturn(userModel);

4.2.9 Mockito.doNothing(): 只是声明一个Mock方法,基本上它告诉Mockito在调用模拟对象中的方法时什么也不做。有时用于void返回方法或没有副作用的方法,或者与正在进行的单元测试无关

  • 4.3 验证结果 4.3.1 Mockito.verify: 校验方法,传入不同的校验规则做不同的校验,如下示例为校验某个方法被调用的次数校验
// 校验 riskService.risk 方法被调用一次
Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));

4.3.2 Mockito.times: 对调用次数的校验,不单独使用,需结合Mockito.verify() 方法

// 校验 riskService.risk 方法被调用一次:Mockito.times(次数)
Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));

4.3.3 Mockito.never: 表示一个方法从来没有调用过,与Mockito.times(0)一样

4.3.4 Mockito.reset: 重置Mock的对象的相关联引用

userService.save(userModel);
// 验证通过
Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));
// 重置已经统计的次数
Mockito.reset(userService);
userService.save(userModel);
// 验证通过
Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));

4.3.5 Mockito.atLeastOnce: 表示某个方法至少调用 X 次

4.3.6 Mockito.atMost: 表示某个方法最多调用 X 次

4.3.7 Mockito.timeout: 调用某个方法超时时间

4.3.8 Mockito.validateMockitoUsage:

5. 结合Junit使用

  • 加载Mock入口:

在使用Junit的时候,如果不制定加载类的Runner,默认使用Junit自己的运行方法。当然,我们在使用Mock的时候,为了结合Junit,我们需要手动制定Runner ,即:@RunWith(MockitoJUnitRunner.class) 或者在@Before方法中设置 MockitoAnnotations.initMocks(this); 两者的作用是等价的

  • 示例demo
// 图片服务
public class RiskService {
    public Map<String, List<String>> risk(Map<String,Object> param){
        System.out.println("riskService");
        return null;
    }
}
// 风控服务
public class RiskService {
    public Map<String, List<String>> risk(Map<String,Object> param){
        System.out.println("riskService");
        return null;
    }
}

// 用户服务
public class UserService {
    private ImageService imageService;
    private RiskService riskService;

    public Map<String, String> save(Map<String, Object> user) {
        Map<String, String> resultMap = new HashMap<>();
        Map<String, List<String>> riskMap = riskService.risk(user);
        if (!CollectionUtils.isEmpty(riskMap)) {
            for (Map.Entry<String, List<String>> entry : riskMap.entrySet()) {
                resultMap.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
            }
            return resultMap;
        }
        boolean flag = imageService.upload((InputStream) user.get("headImage"));
        if (flag) {
            resultMap.put("headImage", "上传头像失败请稍后重试");
            return resultMap;
        }
        // 插入数据库等其他逻辑
        return resultMap;
    }
}

// 测试类
@RunWith(MockitoJUnitRunner.class)
public class MockitoTest {
    @Mock
    private RiskService riskService;
    @Mock
    private ImageService imageService;
    @InjectMocks
//    @Mock
    private UserService userService;

    @Before
    public void init() {
        // 在不使用 @RunWith(MockitoJUnitRunner.class) 的时候需要手动扫描注解
//        MockitoAnnotations.initMocks(this);
//        // 在不使用 @InjectMocks 的时候需要反射赋值,但是这样赋值测试的时候经常会出现一些问题,建议使用 @InjectMocks
//        setField(userService,"riskService",riskService);
//        setField(userService,"imageService",imageService);
        printLog("userService:", userService, ",riskService:", riskService, ",imageService:", imageService);
    }

    @After
    public void after() {
        printLog("执行成功");
    }

    @Test
    public void testWhen() {
        Mockito.when(imageService.upload(Mockito.any())).thenReturn(true);
        Mockito.when(riskService.risk(Mockito.anyMap())).thenReturn(new HashMap());
        userService.save(new HashMap<>());
    }

    @Test
    public void testVerify() {
        testWhen();
        Mockito.verify(imageService, Mockito.times(1)).upload(Mockito.any());
    }

    @Test
    public void testInOrder() {
        // 正确写法
        testWhen();
        final InOrder inOrder = Mockito.inOrder(riskService, imageService);
        inOrder.verify(riskService).risk(Mockito.anyMap());
        inOrder.verify(imageService).upload(Mockito.any());
    }

    @Test
    public void testAtLeastOnce() {
        testWhen();
        testWhen();
        Mockito.verify(riskService, Mockito.atLeastOnce()).risk(Mockito.anyMap());
        Mockito.verify(imageService, Mockito.atLeastOnce()).upload(Mockito.any());
    }

    @Test
    public void testNever() {
        // 正确执行
        Mockito.verify(riskService, Mockito.never()).risk(Mockito.anyMap());
        // 设置引用
//        testWhen();
        // 执行错误
        Mockito.verify(riskService, Mockito.never()).risk(Mockito.anyMap());
    }

    @Test
    public void testArgumentCaptor() {
        // 正常请求
        Map<String, Object> param = new HashMap<>();
        param.put("headImage", Mockito.any(InputStream.class));
        userService.save(param);
        // 对上面执行的参数进行抓取
        ArgumentCaptor<Map> captor = ArgumentCaptor.forClass(Map.class);
        Mockito.verify(riskService).risk(captor.capture());
        Assert.assertEquals(captor.getValue(), param);
    }

    @Test
    public void testVerifyNoMoreInteractions() {
        UserService userService = Mockito.mock(UserService.class);
        // 正确执行
        Mockito.verifyNoMoreInteractions(userService);
        // 重置引用
        Mockito.reset(userService);
        // 正确执行
        Mockito.verifyNoMoreInteractions(userService);
//        // 设置引用
//        userService.save(Mockito.anyMap());
//        // 执行这里会报错
//        Mockito.verifyNoMoreInteractions(userService);
    }

    @Test
    public void testDoExcption() {
        Mockito.doThrow(new NullPointerException()).when(Mockito.mock(UserService.class)).save(null);
    }

    @Test
    public void testa(){
        Map<String, Object> param = new HashMap<>();
        param.put("headImage", Mockito.any(InputStream.class));
        userService.save(param);
    }


    /**
     * 打印日志
     *
     * @param objs
     */
    private void printLog(Object... objs) {
        if (objs == null && objs.length == 0) {
            System.out.println("参数为空,没有什么东西可以打印的");
        }
        StringBuilder sb = new StringBuilder();
        Arrays.stream(objs).forEach(obj -> sb.append(obj));
        System.out.println(sb.toString());
    }

    /**
     * 反射赋值
     *
     * @param t         实例对象
     * @param fieldName 实例对象的属性值
     * @param o         待赋的值
     * @param <T>       T 类型
     * @return 赋值之后的实例对象
     */
    public static <T> T setField(T t, String fieldName, Object o) {
        if (StringUtils.isBlank(fieldName)) {
            return t;
        }
        try {
            Class<?> clsObj = t.getClass();
            final Field[] childFields = clsObj.getDeclaredFields();
            final Optional<Field> childOptionalField = Arrays.asList(childFields).stream().filter(childField -> childField.getName().equals(fieldName)).findFirst();
            if (childOptionalField.isPresent()) {
                Field f = clsObj.getDeclaredField(fieldName);
                f.setAccessible(true);
                f.set(t, o);
                return t;
            }
            // 如果子类没有属性,看看父类是否有该属性
            if (clsObj.getSuperclass() == null) {
                return t;
            }
            T parentObj = (T) clsObj.getSuperclass().newInstance();
            // 递归赋值咯
            parentObj = setField(parentObj, fieldName, o);
            // 拷贝到子类中,然后返回子类
            copyPropertiesIgnoreNull(t, parentObj);
            return t;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return t;
    }

    private static void copyPropertiesIgnoreNull(Object src, Object target) {
        BeanUtils.copyProperties(src, target, getNullPropertyNames(src));
    }

    /**
     * 获取空值属性列表
     *
     * @param source
     * @return
     */
    private static String[] getNullPropertyNames(Object source) {
        final BeanWrapper src = new BeanWrapperImpl(source);
        java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();

        Set<String> emptyNames = new HashSet<>();
        for (java.beans.PropertyDescriptor pd : pds) {
            // 忽略部分字段
            if (Arrays.asList("callback", "callbacks", "classes").contains(pd.getName())) {
                continue;
            }
            Object srcValue = src.getPropertyValue(pd.getName());
            if (srcValue == null) {
                emptyNames.add(pd.getName());
            }
        }
        String[] result = new String[emptyNames.size()];
        return emptyNames.toArray(result);
    }
}

6. 注意事项

总结

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!