作者:陈昌毅(常意)

前语

那些年,为了学分,咱们学会了面向进程编程
那些年,为了就业,咱们学会了面向目标编程
那些年,为了生活,咱们学会了面向工资编程
那些年,为了升职加薪,咱们学会了面向领导编程
那些年,为了完成目标,咱们学会了面向目标编程
……
那些年,咱们学会了唐塞地编程
那些年,咱们编程仅仅为了唐塞

现在,要呼应提高代码质量的召唤,需求提高单元测验的代码掩盖率。当然,咱们要努力提高单元测验的代码掩盖率。至于单元测验用例的有用性,咱们大抵是不用关怀的,因为咱们仅仅面向目标编程。

我曾经阅读过一个Java服务项目,单元测验的代码掩盖率十分高,可是通篇没有一个依靠办法验证(Mockito.verify)、满纸仅存几个数据目标断语(Assert.assertNotNull)。我说,这些都是无效的单元测验用例,底子起不到测验代码BUG和回归验证代码的效果。后来,在一个月黑风高的夜里,一个新增的办法调用,引起了一场血雨腥风。

编写单元测验用例的意图,并不是为了寻求单元测验代码掩盖率,而是为了运用单元测验验证回归代码——企图找出代码中潜藏着的BUG。所以,咱们应该具备工匠精力、怀着一颗敬畏心,编写出有用的单元测验用例。在这篇文章里,作者经过日常的单元测验实践,体系地总结出一套避免编写无效单元测验用例的办法和原则。

单元测验简介

1.1. 单元测验概念

在维基百科中是这样描绘的:

在计算机编程中,单元测验又称为模块测验,是针对程序模块来进行正确性查验的测验工作。程序单元是运用的最小可测验部件。在进程化编程中,一个单元便是单个程序、函数、进程等;关于面向目标编程,最小单元便是办法,包含基类、抽象类、或者派生类中的办法。

1.2. 单元测验事例

首先,经过一个简略的服务代码事例,让咱们认识一下集成测验和单元测验。

1.2.1. 服务代码事例

这儿,以用户服务(UserService)的分页查询用户(queryUser)为例阐明。

@Service
public class UserService {
    /** 界说依靠目标 */
    /** 用户DAO */
    @Autowired
    private UserDAO userDAO;
    /**
     * 查询用户
     * 
     * @param companyId 公司标识
     * @param startIndex 开端序号
     * @param pageSize 分页大小
     * @return 用户分页数据
     */
    public PageDataVO<UserVO> queryUser(Long companyId, Long startIndex, Integer pageSize) {
        // 查询用户数据
        // 查询用户数据: 一共数量
        Long totalSize = userDAO.countByCompany(companyId);
        // 查询接口数据: 数据列表
        List<UserVO> dataList = null;
        if (NumberHelper.isPositive(totalSize)) {
            dataList = userDAO.queryByCompany(companyId, startIndex, pageSize);
        }
        // 回来分页数据
        return new PageDataVO<>(totalSize, dataList);
    }
}

1.2.2. 集成测验用例

很多人以为,凡是用到JUnit测验框架的测验用例都是单元测验用例,于是就写出了下面的集成测验用例。

@Slf4j
@RunWith(PandoraBootRunner.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {ExampleApplication.class})
public class UserServiceTest {
    /** 用户服务 */
    @Autowired
    private UserService userService;
    /**
     * 测验: 查询用户
     */
    @Test
    public void testQueryUser() {
        Long companyId = 123L;
        Long startIndex = 90L;
        Integer pageSize = 10;
        PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
        log.info("testQueryUser: pageData={}", JSON.toJSONString(pageData));
    }
}

集成测验用例首要有以下特色:

  1. 依靠外部环境和数据;
  2. 需求发动运用并初始化测验目标;
  3. 直接运用@Autowired注入测验目标;
  4. 有时候无法验证不确认的回来值,只能靠打印日志来人工核对。

1.2.3. 单元测验用例

选用JUnit+Mockito编写的单元测验用例如下:

@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
    /** 界说静态常量 */
    /** 资源途径 */
    private static final String RESOURCE_PATH = "testUserService/";
    /** 模仿依靠目标 */
    /** 用户DAO */
    @Mock
    private UserDAO userDAO;
    /** 界说测验目标 */
    /** 用户服务 */
    @InjectMocks
    private UserService userService;
    /**
     * 测验: 查询用户-无数据
     */
    @Test
    public void testQueryUserWithoutData() {
        // 模仿依靠办法
        // 模仿依靠办法: userDAO.countByCompany
        Long companyId = 123L;
        Long startIndex = 90L;
        Integer pageSize = 10;
        Mockito.doReturn(0L).when(userDAO).countByCompany(companyId);
        // 调用测验办法
        String path = RESOURCE_PATH + "testQueryUserWithoutData/";
        PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
        String text = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");
        Assert.assertEquals("分页数据不共同", text, JSON.toJSONString(pageData));
        // 验证依靠办法
        // 验证依靠办法: userDAO.countByCompany
        Mockito.verify(userDAO).countByCompany(companyId);
        // 验证依靠目标
        Mockito.verifyNoMoreInteractions(userDAO);
    }
    /**
     * 测验: 查询用户-有数据
     */
    @Test
    public void testQueryUserWithData() {
        // 模仿依靠办法
        String path = RESOURCE_PATH + "testQueryUserWithData/";
        // 模仿依靠办法: userDAO.countByCompany
        Long companyId = 123L;
        Mockito.doReturn(91L).when(userDAO).countByCompany(companyId);
        // 模仿依靠办法: userDAO.queryByCompany
        Long startIndex = 90L;
        Integer pageSize = 10;
        String text = ResourceHelper.getResourceAsString(getClass(), path + "dataList.json");
        List<UserVO> dataList = JSON.parseArray(text, UserVO.class);
        Mockito.doReturn(dataList).when(userDAO).queryByCompany(companyId, startIndex, pageSize);
        // 调用测验办法
        PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
        text = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");
        Assert.assertEquals("分页数据不共同", text, JSON.toJSONString(pageData));
        // 验证依靠办法
        // 验证依靠办法: userDAO.countByCompany
        Mockito.verify(userDAO).countByCompany(companyId);
        // 验证依靠办法: userDAO.queryByCompany
        Mockito.verify(userDAO).queryByCompany(companyId, startIndex, pageSize);
        // 验证依靠目标
        Mockito.verifyNoMoreInteractions(userDAO);
    }
}

单元测验用例首要有以下特色:

  1. 不依靠外部环境和数据;
  2. 不需求发动运用和初始化目标;
  3. 需求用@Mock来初始化依靠目标,用@InjectMocks来初始化测验目标;
  4. 需求自己模仿依靠办法,指定什么参数回来什么值或反常;
  5. 因为测验办法回来值确认,能够直接用Assert相关办法进行断语;
  6. 能够验证依靠办法的调用次数和参数值,还能够验证依靠目标的办法调用是否验证结束。

1.3. 单元测验原则

为什么集成测验不算单元测验呢?咱们能够从单元测验原则上来判别。在业界,常见的单元测验原则有AIR原则和FIRST原则。

1.3.1. AIR原则

AIR原则内容如下:

1、A-Automatic(主动的)

单元测验应该是全主动履行的,而且非交互式的。测验用例一般是被定期履行的,履行进程有必要完全主动化才有含义。输出成果需求人工查看的测验不是一个好的单元测验。单元测验中禁绝运用System.out来进行人肉验证,有必要运用assert来验证。

2、I-Independent(独立的)

单元测验应该坚持的独立性。为了确保单元测验安稳牢靠且便于保护,单元测验用例之间决不能相互调用,也不能对外部资源有所依靠。

3、R-Repeatable(可重复的)

单元测验是能够重复履行的,不能遭到外界环境的影响。单元测验一般会被放入持续集成中,每次有代码提交时单元测验都会被履行。

1.3.2. FIRST原则

FIRST原则内容如下:

1、F-Fast(快速的)

单元测验应该是能够快速运转的,在各种测验办法中,单元测验的运转速度是最快的,大型项意图单元测验一般应该在几分钟内运转结束。

2、I-Independent(独立的)

单元测验应该是能够独立运转的,单元测验用例相互之间无依靠,且对外部资源也无任何依靠。

3、R-Repeatable(可重复的)

单元测验应该能够安稳重复的运转,而且每次运转的成果都是安稳牢靠的。

4、S-SelfValidating(自我验证的)

单元测验应该是用例主动进行验证的,不能依靠人工验证。

5、T-Timely(及时的)

单元测验有必要及时进行编写,更新和保护,以确保用例能够随着业务代码的改变动态的确保质量。

1.3.3. ASCII原则

阿里的夕华先生也提出了一条ASCII原则

1、A-Automatic(主动的)

单元测验应该是全主动履行的,而且非交互式的。

2、S-SelfValidating(自我验证的)

单元测验中有必要运用断语办法来进行正确性验证,而不能依据输出进行人肉验证。

3、C-Consistent(共同的)

单元测验的参数和成果是确认且共同的。

4、I-Independent(独立的)

单元测验之间不能相互调用,也不能依靠履行的先后次第。

5、I-Isolated(阻隔的)

单元测验需求是阻隔的,不要依靠外部资源。

1.3.4. 比照集测和单测

依据上节中的单元测验原则,咱们能够比照集成测验和单元测验的满足情况如下:

那些年,我们写过的无效单元测试

集成测验基本上不一定满足一切单元测验原则;经过上面表格的比照,能够得出以下结论:

  1. 集成测验基本上不一定满足一切单元测验原则;
  2. 单元测验基本上一定都满足一切单元测验原则。

所以,依据这些单元测验原则,能够看出集成测验具有很大的不确认性,不能也不或许完全替代单元测验。其他,集成测验一直是集成测验,即便用于替代单元测验也仍是集成测验,比方:运用H2内存数据库测验DAO办法。

无效单元测验

要想识别无效单元测验,就有必要站在对方的视点思考——如何在确保单元测验掩盖率的前提下,能够更少地编写单元测验代码。那么,就有必要从单元测验编写流程下手,看哪一阶段哪一办法能够偷工减料。

2.1. 单元测验掩盖率

在维基百科中是这样描绘的:

代码掩盖(Code Coverage)是软件测验中的一种衡量,描绘程序中源代码被测验的比例和程度,所得比例称为代码掩盖率。

常用的单元测验掩盖率目标有:

  1. 行掩盖(Line Coverage): 用于衡量被测代码中每一行履行句子是否都被测验到了。
  2. 分支掩盖(Branch Coverage): 用于衡量被测代码中每一个代码分支是否都被测验到了。
  3. 条件掩盖(Condition Coverage): 用于衡量被测代码的条件中每一个子表达式(true和false)是否都被测验到了。
  4. 途径掩盖(Path Coverage): 用于衡量被测代码中的每一个代码分支组合是否都被测验到了。

除此之外,还有办法掩盖(Method Coverage)、类掩盖(Class Coverage)等单元测验掩盖率目标。

下面,用一个简略办法来剖析各个单元测验掩盖率目标:

public static byte combine(boolean b0, boolean b1) {
    byte b = 0;
    if (b0) {
        b |= 0b01;
    }
    if (b1) {
        b |= 0b10;
    }
    return b;
}

那些年,我们写过的无效单元测试

单元测验掩盖率,只能代表被测代码的类、办法、履行句子、代码分支、条件子表达式等是否被履行,可是并不能代表这些代码是否被正确地履行并回来了正确的成果。所以,只看单元测验掩盖率,而不看单元测验有用性,是没有任何含义的。

2.2. 单元测验编写流程

首先,介绍一下作者总结的单元测验编写流程:

那些年,我们写过的无效单元测试

2.2.1. 界说目标阶段

界说目标阶段首要包含:界说被测目标、模仿依靠目标(类成员)、注入依靠目标(类成员)。

那些年,我们写过的无效单元测试

2.2.2. 模仿办法阶段

模仿办法阶段首要包含:模仿依靠目标(参数、回来值和反常)、模仿依靠办法。

那些年,我们写过的无效单元测试

2.2.3. 调用办法阶段

调用办法阶段首要包含:模仿依靠目标(参数)、调用被测办法、验证参数目标(回来值和反常)。

那些年,我们写过的无效单元测试

2.2.4. 验证办法阶段

验证办法阶段首要包含:验证依靠办法、验证数据目标(参数)、验证依靠目标 。

那些年,我们写过的无效单元测试

2.3. 是否能够偷工减料

针对单元测验编写流程的阶段和办法,在不影响单元测验掩盖率的情况,咱们是否能够进行一些偷工减料。

那些年,我们写过的无效单元测试

2.4. 终究能够得出结论

经过上表格,能够得出结论,偷工减料首要集中在验证阶段:

  1. 调用办法阶段
    1. 验证数据目标(回来值和反常)
  1. 验证办法阶段
    1. 验证依靠办法
    2. 验证数据目标(参数)
    3. 验证依靠目标

经过一些合并和拆分,后续将从以下三部分展开:

  1. 验证数据目标(包含特点、参数和回来值);
  2. 验证抛出反常;
  3. 验证依靠办法(包含依靠办法和依靠目标)。

验证数据目标

在单元测验中,验证数据目标是为了验证是否传入了希望的参数值、回来了希望的回来值、设置了希望的特点值。

3.1. 数据目标来历办法

在单元测验中,需求验证的数据目标首要有以下几种来历。

3.1.1. 来历于被测办法的回来值

数据目标来历于调用被测办法的回来值,例如:

PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);

3.1.2. 来历于依靠办法的参数捕获

数据目标来历于验证依靠办法的参数捕获,例如:

ArgumentCaptor<UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).create(userCreateCaptor.capture());
UserDO userCreate = userCreateCaptor.getValue();

3.1.3. 来历于被测目标的特点值

数据目标来历于获取被测目标的特点值,例如:

userService.loadRoleMap();
Map<Long, String> roleMap = Whitebox.getInternalState(userService, "roleMap");

3.1.4. 来历于请求参数的特点值

数据目标来历于获取请求参数的特点值,例如:

OrderContext orderContext = new OrderContext();
orderContext.setOrderId(12345L);
orderService.supplyProducts(orderContext);
List<ProductDO> productList = orderContext.getProductList();

当然,数据目标还有其它来历办法,这儿就不再一一举例了。

3.2. 数据目标验证办法

在调用被测办法时,需求对回来值和反常进行验证;在验证办法调用时,也需求对捕获的参数值进行验证。

3.2.1. 验证数据目标空值

JUnit供给Assert.assertNull和Assert.assertNotNull办法来验证数据目标空值。

// 1. 验证数据目标为空
Assert.assertNull("用户标识有必要为空", userId);
// 2. 验证数据目标非空
Assert.assertNotNull("用户标识不能为空", userId);

3.2.2. 验证数据目标布尔值

JUnit供给Assert.assertTrue和Assert.assertFalse办法来验证数据目标布尔值的真假。

// 1. 验证数据目标为真
Assert.assertTrue("回来值有必要为真", NumberHelper.isPositive(1));
// 2. 验证数据目标为假
Assert.assertFalse("回来值有必要为假", NumberHelper.isPositive(-1));

3.2.3. 验证数据目标引证

JUnit供给Assert.assertSame和Assert.assertNotSame办法来验证数据目标引证是否共同。

// 1. 验证数据目标共同
Assert.assertSame("用户有必要共同", expectedUser, actualUser);
// 2. 验证数据目标不共同
Assert.assertNotSame("用户不能共同", expectedUser, actualUser);

3.2.4. 验证数据目标取值

JUnit供给Assert.assertEquals、Assert.assertNotEquals、Assert.assertArrayEquals办法组,能够用来验证数据目标值是否相等。

// 1. 验证简略数据目标
Assert.assertNotEquals("用户称号不共同", "admin", userName);
Assert.assertEquals("账户金额不共同", 10000.0D, accountAmount, 1E-6D);
// 2. 验证简略集合目标
Assert.assertArrayEquals("用户标识列表不共同", new Long[] {1L, 2L, 3L}, userIds);
Assert.assertEquals("用户标识列表不共同", Arrays.asList(1L, 2L, 3L), userIdList);
// 3. 验证杂乱数据目标
Assert.assertEquals("用户标识不共同", Long.valueOf(1L), user.getId());
Assert.assertEquals("用户称号不共同", "admin", user.getName());
...
// 4. 验证杂乱集合目标
Assert.assertEquals("用户列表长度不共同", expectedUserList.size(), actualUserList.size());
UserDO[] expectedUsers = expectedUserList.toArray(new UserDO[0]);
UserDO[] actualUsers = actualUserList.toArray(new UserDO[0]);
for (int i = 0; i < actualUsers.length; i++) { 
     Assert.assertEquals(String.format("用户 (%s) 标识不共同", i), expectedUsers[i].getId(), actualUsers[i].getId()); 
     Assert.assertEquals(String.format("用户 (%s) 称号不共同", i), expectedUsers[i].getName(), actualUsers[i].getName());
     ...
};
// 5. 经过序列化验证数据目标
String text = ResourceHelper.getResourceAsString(getClass(), "userList.json");
Assert.assertEquals("用户列表不共同", text, JSON.toJSONString(userList));;
// 6. 验证数据目标私有特点字段
Assert.assertEquals("根底包不共同", "com.alibaba.example", Whitebox.getInternalState(configurer, "basePackage"));

当然,数据目标还有其它验证办法,这儿就不再一一举例了。

3.3. 验证数据目标问题

这儿,以分页查询公司用户为例,来阐明验证数据目标时所存在的问题。

代码事例:

public PageDataVO<UserVO> queryUser(Long companyId, Long startIndex, Integer pageSize) {
    // 查询用户数据
    // 查询用户数据: 一共数量
    Long totalSize = userDAO.countByCompany(companyId);
    // 查询接口数据: 数据列表
    List<UserVO> dataList = null;
    if (NumberHelper.isPositive(totalSize)) {
        List<UserDO> userList = userDAO.queryByCompany(companyId, startIndex, pageSize);
        dataList = userList.stream().map(UserService::convertUser)
            .collect(Collectors.toList());
    }
    // 回来分页数据
    return new PageDataVO<>(totalSize, dataList);
}
private static UserVO convertUser(UserDO userDO) {
    UserVO userVO = new UserVO();
    userVO.setId(userDO.getId());
    userVO.setName(userDO.getName());
    userVO.setDesc(userDO.getDesc());
    ...
    return userVO;
}

3.3.1. 不验证数据目标

不和事例: 很多人为了偷闲,对数据目标不进行任何验证。

// 调用测验办法
userService.queryUser(companyId, startIndex, pageSize);

存在问题:

无法验证数据目标是否正确,比方被测代码进行了以下修正:

// 回来分页数据
return null;

3.3.2. 验证数据目标非空

不和事例:

已然不验证数据目标有问题,那么我就简略地验证一下数据目标非空。

// 调用测验办法
PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
Assert.assertNotNull("分页数据不为空", pageData);

存在问题:

无法验证数据目标是否正确,比方被测代码进行了以下修正:

// 回来分页数据
return new PageDataVO<>();

3.3.3. 验证数据目标部分特点

不和事例:

已然简略地验证数据目标非空不可,那么我就验证数据目标的部分特点。

// 调用测验办法
PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
Assert.assertEquals("数据总量不为空", totalSize, pageData.getTotalSize());

存在问题:

无法验证数据目标是否正确,比方被测代码进行了以下修正:

// 回来分页数据
return new PageDataVO<>(totalSize, null);

3.3.4. 验证数据目标全部特点

不和事例:

验证数据目标部分特点也不可,那我验证数据目标一切特点总行了吧。

// 调用测验办法
PageDataVO<UserVO> pageData = userService.queryUser(companyId);
Assert.assertEquals("数据总量不为空", totalSize, pageData.getTotalSize());
Assert.assertEquals("数据列表不为空", dataList, pageData.getDataList());

存在问题:

上面的代码看起来很完美,验证了PageDataVO中两个特点值totalSize和dataList。可是,假如有一天在PageDataVO中添加了startIndex和pageSize,就无法验证这两个新特点是否赋值正确。代码如下:

// 回来分页数据
return new PageDataVO<>(startIndex, pageSize, totalSize, dataList);

补白: 本办法仅适用于特点字段不可变的数据目标

3.3.5. 完美地验证数据目标

关于数据目标特点字段新增,有没有完美的验证计划?有的!答案便是运用JSON序列化,然后比较JSON文本内容。假如数据目标新增了特点字段,必然会提示JSON字符串不共同。

完美事例:

// 调用测验办法
PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
text = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");
Assert.assertEquals("分页数据不共同", text, JSON.toJSONString(pageData));

补白: 本办法仅适用于特点字段可变的数据目标。

3.4. 模仿数据目标原则

因为没有模仿数据目标章节,这儿在验证数据目标章节中插入了模仿数据目标原则。

3.4.1. 除触发条件分支外,模仿目标一切特点值不能为空

在上一节中,咱们展示了如何完美地验证数据目标。可是,这种办法真正完美吗?答案是否定。

比方:咱们把userDAO.queryByCompany办法回来的uesrList的一切UserDO目标的特点值name和desc赋值为空,再把convertUser办法的name和desc赋值做一下交换,上面的单元测验用例是无法验证出来的。

private static UserVO convertUser(UserDO userDO) {
    UserVO userVO = new UserVO();
    userVO.setId(userDO.getId());
    userVO.setName(userDO.getDesc());
    userVO.setDesc(userDO.getName());
    ...
    return userVO;
}

所以,在单元测验中,除触发条件分支外,模仿目标一切特点值不能为空。

3.4.2. 新增数据类特点字段时,有必要模仿数据目标的特点值

在上面的事例中,假如UserDO和UserVO新增了特点字段age(用户年龄),且新增了赋值句子如下:

userVO.setAge(userDO.getAge());

假如仍是用原有的数据目标履行单元测验,咱们会发现单元测验用例履行经过。这是因为,因为特点字段age为空,赋值不赋值没有任何不同。所以,新增特点类特点字段是,有必要模仿数据目标的特点值。

留意: 假如用JSON字符串比照,且设置输出空字段,是能够触发单元测验用例履行失利的。

3.5. 验证数据目标原则

3.5.1. 有必要验证一切数据目标

在单元测验中,有必要验证一切数据目标:

  1. 来历于被测办法的回来值
  2. 来历于依靠办法的参数捕获
  3. 来历于被测目标的特点值
  4. 来历于请求参数的特点值。

详细事例能够参阅《数据目标来历办法》章节。

3.5.2. 有必要运用清晰语义的断语

在运用断语验证数据目标时,有必要运用确认语义的断语,不能运用不清晰语义的断语。

正例:

Assert.assertTrue("回来值不为真", NumberHelper.isPositive(1));
Assert.assertEquals("用户不共同", user, userService.getUser(userId));

反例:

Assert.assertNotNull("用户不能为空", userService.getUser(userId));
Assert.assertNotEquals("用户不能共同", user, userService.getUser(userId));

谨防一些企图绕过本条原则的事例,企图用清晰语义的断语去做不清晰语义的判别。

Assert.assertTrue("用户不能为空", Objects.nonNull(userService.getUser(userId)));

3.5.3. 尽量选用整体验证办法

假如一个模型类,会依据业务需求新增字段。那么,针对这个模型类所对应的数据目标,尽量选用整体验证办法。

正例:

UserVO user = userService.getUser(userId);
String text = ResourceHelper.getResourceAsString(getClass(), path + "user.json");
Assert.assertEquals("用户不共同", text, JSON.toJSONString(user));

反例:

UserVO user = userService.getUser(userId);
Assert.assertEquals("用户标识不共同", Long.valueOf(123L), user.getId());
Assert.assertEquals("用户称号不共同", "changyi", user.getName());
...

上面这种数据验证办法,假如模型类删去了特点字段,是能够验证出来的。可是,假如模型类添加了字段,是无法验证出来的。所以,假如选用了这种验证办法,在新增了模型类特点字段后,需求整理并补全测验用例。不然,在运用单元测验用例回归代码时,它将会告知你这儿没有任何问题

验证抛出反常

反常作为Java语言的重要特性,是Java语言健壮性的重要体现。捕获并验证抛出反常,也是测验用例的一种。所以,在单元测验中,也需求对抛出反常进行验证。

4.1. 抛出反常来历办法

4.1.1. 来历于特点字段的判别

判别特点字段是否非法,不然抛出反常。

private Map<String, MessageHandler> messageHandlerMap = ...;
public void handleMessage(Message message) {
    ...
    // 判别处理器映射非空
    if (CollectionUtils.isEmpty(messageHandlerMap)) {
        throw new ExampleException("音讯处理器映射不能为空");
    }
    ...
}

4.1.2. 来历于输入参数的判别

判别输入参数是否合法,不然抛出反常。

public void handleMessage(Message message) {
    ...
    // 判别获取处理器非空
    MessageHandler messageHandler = messageHandlerMap.get(message.getType());
    if (CollectionUtils.isEmpty(messageHandler)) {
        throw new ExampleException("获取音讯处理器不能为空");
    }
    ...
}

留意: 这儿选用的是Spring框架供给的Assert类,跟if-throw句子的效果相同。

4.1.3. 来历于回来值的判别

判别回来值是否合法,不然抛出反常。

public void handleMessage(Message message) {
    ...
    // 进行音讯处理器处理
    boolean result = messageHandler.handleMessage(message);
    if (!reuslt) {
        throw new ExampleException("处理音讯反常");
    }
    ...
}

4.1.4.来历于模仿办法的调用

调用模仿的依靠办法时,或许模仿的依靠办法会抛出反常。

public void handleMessage(Message message) {
    ...
    // 进行音讯处理器处理
    boolean result = messageHandler.handleMessage(message); // 直接抛出反常
    ...
}

这儿,能够进行反常捕获处理,或打印输出日志,或继续抛出反常。

4.1.5. 来历于静态办法的调用

有时候,静态办法调用也有或许抛出反常。

// 或许会抛出IOException
String response = HttpHelper.httpGet(url, parameterMap);

除此之外,还有其他抛出反常来历办法,这儿不再累述。

4.2. 抛出反常验证办法

在单元测验中,一般存在四种验证抛出反常办法。

4.2.1. 经过try-catch句子验证抛出反常

Java单元测验用例中,最简略直接的反常捕获办法便是运用try-catch句子。

@Test
public void testCreateUserWithException() {
    // 模仿依靠办法
    Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());
    // 调用测验办法
    UserCreateVO userCreate = new UserCreateVO();
    try {
        userCreate.setName("changyi");
        userCreate.setDescription("Java Programmer");
        userService.createUser(userCreate);
    } catch (ExampleException e) {
        Assert.assertEquals("反常编码不共同", ErrorCode.OBJECT_EXIST, e.getCode());
        Assert.assertEquals("反常音讯不共同", "用户已存在", e.getMessage());
    }
    // 验证依靠办法
    Mockito.verify(userDAO).existName(userCreate.getName());
}

4.2.2. 经过@Test注解验证抛出反常

JUnit的@Test注解供给了一个expected特点,能够指定一个希望的反常类型,用来捕获并验证反常。

@Test(expected = ExampleException.class)
public void testCreateUserWithException() {
    // 模仿依靠办法
    Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());
    // 调用测验办法
    UserCreateVO userCreate = new UserCreateVO();
    userCreate.setName("changyi");
    userCreate.setDescription("Java Programmer");
    userService.createUser(userCreate);
    // 验证依靠办法(不会履行)
    Mockito.verify(userDAO).existName(userCreate.getName());
}

留意: 测验用例在履行到 userService.createUser办法后将跳出办法,导致后续验证句子无法履行。所以,这种办法无法验证反常编码、音讯、原因等内容,也无法验证依靠办法及其参数。

4.2.3. 经过@Rule注解验证抛出反常

假如想要验证反常原因和音讯,就需求选用@Rule注解界说ExpectedException目标,然后在测验办法的前面声明要捕获的反常类型、原因和音讯。

@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void testCreateUserWithException1() {
    // 模仿依靠办法
    Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());
    // 调用测验办法
    UserCreateVO userCreate = new UserCreateVO();
    userCreate.setName("changyi");
    userCreate.setDescription("Java Programmer");
    exception.expect(ExampleException.class);
    exception.expectMessage("用户已存在");
    userService.createUser(userCreate);
    // 验证依靠办法(不会履行)
    Mockito.verify(userDAO).existName(userCreate.getName());
}

留意: 测验用例在履行到 userService.createUser办法后将跳出办法,导致后续验证句子无法履行。所以,这种办法无法验证依靠办法及其参数。因为ExpectedException的验证办法只支持验证反常类型、原因和音讯,无法验证反常的自界说特点字段值。现在,JUnit官方主张运用Assert.assertThrows替换。

4.2.4. 经过Assert.assertThrows办法验证抛出反常

在最新版的JUnit中,供给了一个更为简练的反常验证办法——Assert.assertThrows办法。

@Test
public void testCreateUserWithException() {
    // 模仿依靠办法
    Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());
    // 调用测验办法
    UserCreateVO userCreate = new UserCreateVO();
    userCreate.setName("changyi");
    userCreate.setDescription("Java Programmer");
    ExampleException exception = Assert.assertThrows("反常类型不共同", ExampleException.class, () -> userService.createUser(userCreate));
    Assert.assertEquals("反常编码不共同", ErrorCode.OBJECT_EXIST, exception.getCode());
    Assert.assertEquals("反常音讯不共同", "用户已存在", exception.getMessage());
    // 验证依靠办法
    Mockito.verify(userDAO).existName(userCreate.getName());
}

4.2.5. 四种抛出反常验证办法比照

依据不同的验证反常功能项,对四种抛出反常验证办法比照。成果如下:

那些年,我们写过的无效单元测试

综上所述,选用Assert.assertThrows办法验证抛出反常是最佳的,也是JUnit官方推荐运用的。

4.3. 验证抛出反常问题

这儿,以创立用户时抛出反常为例,来阐明验证抛出反常时所存在的问题。代码事例:

private UserDAO userDAO;
public void createUser(@Valid UserCreateVO userCreateVO) {
    try {
        UserDO userCreateDO = new UserDO();
        userCreateDO.setName(userCreateVO.getName());
        userCreateDO.setDesc(userCreateVO.getDesc());
        userDAO.create(userCreateDO);
    } catch (RuntimeException e) {
        log.error("创立用户反常: userName={}", userName, e)
        throw new ExampleException(ErrorCode.DATABASE_ERROR, "创立用户反常", e);
    }
}

4.3.1. 不验证抛出反常类型

不和事例:

在验证抛出反常时,很多人运用@Test注解的expected特点,而且指定取值为Exception.class,首要原因是:

  1. 单元测验用例的代码简练,只要一行@Test注解;
  2. 不管抛出什么反常,都能确保单元测验用例经过。
@Test(expected = Exception.class)
public void testCreateUserWithException() {
    // 模仿依靠办法
    Throwable e = new RuntimeException();
    Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));
    // 调用测验办法
    UserCreateVO userCreateVO = ...;
    userService.createUser(userCreate);
}

存在问题: 上面用例指定了通用反常类型,没有对抛出反常类型进行验证。所以,假如把ExampleException反常改为RuntimeException反常,该单元测验用例是无法验证出来的。

throw new RuntimeException("创立用户反常", e);

4.3.2. 不验证抛出反常特点

不和事例: 已然需求验证反常类型,简略地指定@Test注解的expected特点为ExampleException.class即可。

@Test(expected = ExampleException.class)
public void testCreateUserWithException() {
    // 模仿依靠办法
    Throwable e = new RuntimeException();
    Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));
    // 调用测验办法
    UserCreateVO userCreateVO = ...;
    userService.createUser(userCreate);
}

存在问题:

上面用例只验证了反常类型,没有对抛出反常特点字段(反常音讯、反常原因、过错编码等)进行验证。所以,假如把过错编码DATABASE_ERROR(数据库过错) 改为PARAMETER_ERROR(参数过错) ,该单元测验用例是无法验证出来的。

throw new ExampleException(ErrorCode.PARAMETER_ERROR, "创立用户反常", e);

4.3.3. 只验证抛出反常部分特点

不和事例:

假如要验证反常特点,就有必要用Assert.assertThrows办法捕获反常,并对反常的常用特点进行验证。可是,有些人为了偷闲,只验证抛出反常部分特点。

// 模仿依靠办法
Throwable e = new RuntimeException();
Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));
// 调用测验办法
UserCreateVO userCreateVO = ...;
ExampleException exception = Assert.assertThrows("反常类型不共同", ExampleException.class, () -> userService.createUser(userCreateVO));
Assert.assertEquals("反常编码不共同", ErrorCode.DATABASE_ERROR, exception.getCode());

存在问题:

上面用例只验证了反常类型和过错编码,假如把过错音讯 “创立用户反常” 改为 “创立用户过错” ,该单元测验用例是无法验证出来的。

throw new ExampleException(ErrorCode.DATABASE_ERROR, "创立用户过错", e);

4.3.4. 不验证抛出反常原因

不和事例:

先捕获抛出反常,再验证反常编码和反常音讯,看起来很完美了。

// 模仿依靠办法
Throwable e = new RuntimeException();
Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));
// 调用测验办法
UserCreateVO userCreateVO = ...;
ExampleException exception = Assert.assertThrows("反常类型不共同", ExampleException.class, () -> userService.createUser(userCreateVO));
Assert.assertEquals("反常编码不共同", ErrorCode.OBJECT_EXIST, exception.getCode());
Assert.assertEquals("反常音讯不共同", “创立用户反常”, exception.getMessage());

存在问题:

经过代码能够看出,在抛出ExampleException反常时,最终一个参数e是咱们模仿的userService.createUser办法抛出的RuntimeException反常。可是,咱们没有对抛出反常原因进行验证。假如修正代码,把最终一个参数e去掉,上面的单元测验用例是无法验证出来的。

throw new ExampleException(ErrorCode.DATABASE_ERROR, "创立用户反常");

4.3.5. 不验证相关办法调用

不和事例:

很多人以为,验证抛出反常就只验证抛出反常,验证依靠办法调用不是有必要的。

// 模仿依靠办法
Throwable e = new RuntimeException();
Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));
// 调用测验办法
UserCreateVO userCreateVO = ...;
ExampleException exception = Assert.assertThrows("反常类型不共同", ExampleException.class, () -> userService.createUser(userCreateVO));
Assert.assertEquals("反常编码不共同", ErrorCode.OBJECT_EXIST, exception.getCode());
Assert.assertEquals("反常音讯不共同", “创立用户反常”, exception.getMessage());
Assert.assertEquals("反常原因不共同", e, exception.getCause());

存在问题:

假如不验证相关办法调用,如何能证明代码走过这个分支?比方:咱们在创立用户之前,查看用户称号无效并抛出反常。

// 查看用户称号有用
String userName = userCreateVO.getName();
if (StringUtils.length(userName) < USER_NAME_LENGTH) {
    throw new ExampleException(ErrorCode.INVALID_USERNAME, "无效用户称号");
}

4.3.6. 完美地验证抛出反常

一个完美的反常验证,除对反常类型、反常特点、反常原因等进行验证外,还需对抛出反常前的依靠办法调用进行验证。

完美事例:

// 模仿依靠办法
Throwable e = new RuntimeException();
Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));
// 调用测验办法
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateVO.json");
UserCreateVO userCreateVO = JSON.parseObject(text, UserCreateVO.class);
ExampleException exception = Assert.assertThrows("反常类型不共同", ExampleException.class, () -> userService.createUser(userCreateVO));
Assert.assertEquals("反常编码不共同", ErrorCode.OBJECT_EXIST, exception.getCode());
Assert.assertEquals("反常音讯不共同", “创立用户反常”, exception.getMessage());
Assert.assertEquals("反常原因不共同", e, exception.getCause());
// 验证依靠办法
ArgumentCaptor<UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).create(userCreateCaptor.capture());
text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateDO.json");
Assert.assertEquals("用户创立不共同", text, JSON.toJSONString(userCreateCaptor.getValue()));

4.4. 验证抛出反常原则

4.4.1. 有必要验证一切抛出反常

在单元测验中,有必要验证一切抛出反常:

  1. 来历于特点字段的判别
  2. 来历于输入参数的判别
  3. 来历于回来值的判别
  4. 来历于模仿办法的调用
  5. 来历于静态办法的调用

详细内容能够参阅 《抛出反常来历办法》 章节。

4.4.2. 有必要验证反常类型、反常特点、反常原因

在验证抛出反常时,有必要验证反常类型、反常特点、反常原因等。

正例:

ExampleException exception = Assert.assertThrows("反常类型不共同", ExampleException.class, () -> userService.createUser(userCreateVO));
Assert.assertEquals("反常编码不共同", ErrorCode.OBJECT_EXIST, exception.getCode());
Assert.assertEquals("反常音讯不共同", "用户已存在", exception.getMessage());
Assert.assertEquals("反常原因不共同", e, exception.getCause());

反例:

@Test(expected = ExampleException.class)
public void testCreateUserWithException() {
    ...
    userService.createUser(userCreateVO);
}

4.4.3. 验证抛出反常后,有必要验证相关办法调用

在验证抛出反常后,有必要验证相关办法调用,来确保单元测验用例走的是希望分支。

正例:

/ 调用测验办法
...
// 验证依靠办法
ArgumentCaptor<UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).create(userCreateCaptor.capture());
text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateDO.json");
Assert.assertEquals("用户创立不共同", text, JSON.toJSONString(userCreateCaptor.getValue()));

验证办法调用

在单元测验中,验证办法调用是为了验证依靠办法的调用次数和次序以及是否传入了希望的参数值。

5.1. 办法调用来历办法

5.1.1. 来历于注入目标的办法调用

最常见的办法调用便是对注入依靠目标的办法调用。

private UserDAO userDAO;
public UserVO getUser(Long userId) {
    UserDO user = userDAO.get(userId); // 办法调用
    return convertUser(user);
}

5.1.2. 来历于输入参数的办法调用

有时候,也能够经过输入参数传入依靠目标,然后调用依靠目标的办法。

public <T> List<T> executeQuery(String sql, DataParser<T> dataParser) {
    List<T> dataList = new ArrayList<>();
    List<Record> recordList = SQLTask.getResult(sql);
    for (Record record : recordList) {
        T data = dataParser.parse(record); // 办法调用
        if (Objects.nonNull(data)) {
            dataList.add(data);
        }
    }
    return dataList;
}

5.1.3. 来历于回来值的办法调用

private UserHsfService userHsfService;
public User getUser(Long userId) {
    Result<User> result = userHsfService.getUser(userId);
    if (!result.isSuccess()) { // 办法调用1
        throw new ExampleException("获取用户反常");
    }
    return result.getData(); // 办法调用2
}

5.1.4. 来历于静态办法的调用

在Java中,静态办法是指被static润饰的成员办法,不需求经过目标实例就能够被调用。在日常代码中,静态办法调用一向占有一定的比例。

String text = JSON.toJSONString(user); // 办法调用

5.2. 办法调用验证办法

在单元测验中,验证依靠办法调用是确认模仿目标的依靠办法是否被依照预期调用的进程。

5.2.1. 验证依靠办法的调用参数

// 1.验证无参数依靠办法调用
Mockito.verify(userDAO).deleteAll();
// 2.验证指定参数依靠办法调用
Mockito.verify(userDAO).delete(userId);
// 3.验证任意参数依靠办法调用
Mockito.verify(userDAO).delete(Mockito.anyLong());
// 4.验证可空参数依靠办法调用
Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.nullable(Long.class));
// 5.验证必空参数依靠办法调用
Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.isNull());
// 6.验证可变参数依靠办法调用
Mockito.verify(userService).delete(1L, 2L, 3L);
Mockito.verify(userService).delete(Mockito.any(Long.class));  // 匹配一个
Mockito.verify(userService).delete(Mockito.<Long>any()); // 匹配多个

5.2.2. 验证依靠办法的调用次数

// 1.验证依靠办法默许调用1次
Mockito.verify(userDAO).delete(userId);
// 2.验证依靠办法从不调用
Mockito.verify(userDAO, Mockito.never()).delete(userId);
// 3.验证依靠办法调用n次
Mockito.verify(userDAO, Mockito.times(n)).delete(userId);
// 4.验证依靠办法调用至少1次
Mockito.verify(userDAO, Mockito.atLeastOnce()).delete(userId);
// 5.验证依靠办法调用至少n次
Mockito.verify(userDAO, Mockito.atLeast(n)).delete(userId);
// 6.验证依靠办法调用最多1次
Mockito.verify(userDAO, Mockito.atMostOnce()).delete(userId);
// 7.验证依靠办法调用最多n次
Mockito.verify(userDAO, Mockito.atMost(n)).delete(userId); 
// 8.验证依靠办法调用指定n次
Mockito.verify(userDAO, Mockito.call(n)).delete(userId); // 不会被标记为已验证
// 9.验证依靠目标及其办法仅调用1次
Mockito.verify(userDAO, Mockito.only()).delete(userId);

5.2.3. 验证依靠办法并捕获参数值

// 1.运用ArgumentCaptor.forClass办法界说参数捕获器
ArgumentCaptor<UserDO> userCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).modify(userCaptor.capture());
UserDO user = userCaptor.getValue();
// 2.运用@Captor注解界说参数捕获器
@Captor
private ArgumentCaptor<UserDO> userCaptor;
// 3.捕获多次办法调用的参数值列表
ArgumentCaptor<UserDO> userCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO, Mockito.atLeastOnce()).modify(userCaptor.capture());
List<UserDO> userList = userCaptor.getAllValues();

5.2.4. 验证其它类型的依靠办法调用

// 1.验证 final 办法调用
final办法的验证跟普通办法相似。
// 2.验证私有办法调用
PowerMockito.verifyPrivate(mockClass, times(1)).invoke("unload", any(List.class));
// 3.验证构造办法调用
PowerMockito.verifyNew(MockClass.class).withNoArguments();
PowerMockito.verifyNew(MockClass.class).withArguments(someArgs);
// 4.验证静态办法调用
PowerMockito.verifyStatic(StringUtils.class);
StringUtils.isEmpty(string);

5.2.5. 验证依靠目标没有更多办法调用

// 1.验证模仿目标没有任何办法调用
Mockito.verifyNoInteractions(idGenerator, userDAO);
// 2.验证模仿目标没有更多办法调用
Mockito.verifyNoMoreInteractions(idGenerator, userDAO);

5.3. 验证依靠办法问题

这儿,以cacheUser(缓存用户)为例,来阐明验证依靠办法时所存在的问题。

代码事例:

private UserCache userCache;
public boolean cacheUser(List<User> userList) {
    boolean result = true;
    for (User user : userList) {
        result = result && userCache.set(user.getId(), user);
    }
    return result;
}

5.3.1. 不验证依靠办法调用

不和事例:

有些人觉得,已然现已模仿了依靠办法,而且被测办法现已依照预期回来了值,就没有必要对依靠办法进行验证。

// 模仿依靠办法
Mockito.doReturn(true).when(userCache).set(Mockito.anyLong(), Mockito.any(User.class));
// 调用测验办法
List<User> userList = ...;
Assert.assertTrue("处理成果不为真", userService.cacheUser(userList));
// 不验证依靠办法

存在问题:

模仿了依靠办法,而且被测办法现已依照预期回来了值,并不代表这个依靠办法被调用或者被正确地调用。比方:在for循环之前,把userList置为空列表,这个单元测验用例是无法验证出来的。

// 铲除用户列表
userList = Collections.emptyList();

5.3.2. 不验证依靠办法调用次数

不和事例:

有些很喜爱用Mockito.verify的验证至少一次和任意参数的组合,因为它能够适用于任何依靠办法调用的验证。

// 验证依靠办法
Mockito.verify(userCache, Mockito.atLeastOnce()).set(Mockito.anyLong(), Mockito.any(User.class));

存在问题:

这种办法尽管适用于任何依靠办法调用的验证,可是基本上没有任何实质效果。

比方:咱们不小心,把缓存句子写了两次,这个单元测验用例是无法验证出来的。

// 写了两次缓存
result = result && userCache.set(user.getId(), user);
result = result && userCache.set(user.getId(), user);

5.3.3. 不验证依靠办法调用参数

不和事例:

已然说验证至少一次有问题,那我就指定一下验证次数。

// 验证依靠办法
Mockito.verify(userCache, Mockito.times(userList.size())).set(Mockito.anyLong(), Mockito.any(User.class));

存在问题:

验证办法次数的问题尽管处理了,可是验证办法参数的问题任然存在。

比方:咱们不小心,把循环缓存每一个用户写成循环缓存第一个用户,这个单元测验用例是无法验证出来的。

User user = userList.get(0);
for (int i = 0; i < userList.size(); i++) {
    result = result && userCache.set(user.getId(), user);
}

5.3.4. 不验证一切依靠办法调用

不和事例:

不能用任意参数验证办法,那只好用实际参数验证办法了。可是,验证一切依靠办法调用代码太多,所以验证一两个依靠办法调用意思意思就行了。

Mockito.verify(userCache).set(user1.getId(), user1);
Mockito.verify(userCache).set(user2.getId(), user2);

存在问题:

假如只验证了一两个办法调用,只能确保这一两个办法调用没有问题。

比方:咱们不小心,在for循环之后,还进行了一个用户缓存。

// 缓存最终一个用户
User user = userList.get(userList.size() - 1);
userCache.set(user.getId(), user);

5.3.5. 验证一切依靠办法调用

不和事例:

已然不验证一切办法调用有问题,那我就把一切办法调用验证了吧。

for (User user : userList) {
    Mockito.verify(userCache).set(user.getId(), user);
}

存在问题:

一切办法调用都被验证了,看起来应该没有问题了。可是,假如缓存用户办法中,存在其他办法调用。比方:咱们在进入缓存用户办法之前,新增了铲除一切用户缓存,这个单元测验用是无法验证的。

// 删去一切用户缓存
userCache.clearAll();

5.3.6. 完美地验证依靠办法调用

验证一切的办法调用,只能确保现在的逻辑没有问题。假如触及新增办法调用,这个单元测验用例是无法验证出来的。一切,咱们需求验证一切依靠目标没有更多办法调用。

完美事例:

// 验证依靠办法
ArgumentCaptor<Long> userIdCaptor = ArgumentCaptor.forClass(Long.class);
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
Mockito.verify(userCache, Mockito.atLeastOnce()).set(userIdCaptor.capture(), userCaptor.capture());
Assert.assertEquals("用户标识列表不共同", userIdList, userIdCaptor.getAllValues());
Assert.assertEquals("用户信息列表不共同", userList, userCaptor.getAllValues());
// 验证依靠目标
Mockito.verifyNoMoreInteractions(userCache);

留意: 运用ArgumentCaptor(参数捕获器),不但能够验证参数,还能够验证调用次数和次序。

5.4. 验证办法调用原则

5.4.1. 有必要验证一切的模仿办法调用

在单元测验中,触及到的一切模仿办法都要被验证:

  1. 来历于注入目标的办法调用
  2. 来历于输入参数的办法调用
  3. 来历于回来值的办法调用
  4. 来历于静态办法的调用

详细事例能够参阅 《办法调用来历办法》 章节。

5.4.2. 有必要验证一切的模仿目标没有更多办法调用

在单元测验中,为了防止被测办法中存在或新增其他办法调用,有必要验证一切的模仿目标没有更多办法调用。

正例:

// 验证依靠目标
Mockito.verifyNoMoreInteractions(userDAO, userCache);

补白:

作者喜爱在@After办法中对一切模仿目标进行验证,这样就不用在每个单元测验用例中验证模仿目标。

@After
public void afterTest() {
    Mockito.verifyNoMoreInteractions(userDAO, userCache);
}

可惜Mockito.verifyNoMoreInteractions不支持无参数就验证一切模仿目标的功能,不然这段代码会变得更简练。

5.4.3. 有必要运用清晰语义的参数值或匹配器

验证依靠办法时,有必要运用清晰语义的参数值或匹配器,不能运用任何不清晰语义的匹配器,比方:any系列参数匹配器。

正例:

Mockito.verify(userDAO).get(userId);
Mockito.verify(userDAO).query(Mockito.eq(companyId), Mockito.isNull());

反例:

Mockito.verify(userDAO).get(Mockito.anyLong());
Mockito.verify(userDAO).query(Mockito.anyLong(), Mockito.isNotNull());

后记

最终,依据本文所表达的观点,即兴赋诗七言绝句一首:

《单元测验》
单元测验分真假,
工匠精力贯一直。
掩盖寻求非意图,
回归验证显奇功。

意思是:

一定要知道如何去分辩单元测验的真假,
一定要把工匠精力遵循单元测验的一直。
寻求单测掩盖率并不是单元测验的意图,
回归验证代码才干显示单元测验的功效。