关于阿里云:那些年我们写过的无效单元测试

4次阅读

共计 28775 个字符,预计需要花费 72 分钟才能阅读完成。

作者:陈昌毅(常意)

前言

那些年,为了学分,咱们学会了 面向过程编程
那些年,为了待业,咱们学会了 面向对象编程
那些年,为了生存,咱们学会了 面向工资编程
那些年,为了升职加薪,咱们学会了 面向领导编程
那些年,为了实现指标,咱们学会了 面向指标编程
……
那些年,咱们学会了 搪塞地编程
那些年,咱们 编程只是为了搪塞

当初,要响应进步代码品质的号召,须要晋升单元测试的代码覆盖率。当然,咱们要努力提高单元测试的代码覆盖率。至于单元测试用例的有效性,咱们大抵是不必关怀的,因为咱们只是 面向指标编程。

我已经浏览过一个 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());

后记

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

《单元测试》
单元测试分虚实,
工匠精力贯始终。
笼罩谋求非目标,
回归验证显奇功。

意思是:

肯定要晓得如何去分辨单元测试的虚实,
肯定要把工匠精力贯彻单元测试的始终。
谋求单测覆盖率并不是单元测试的目标,
回归验证代码能力彰显单元测试的效用。

正文完
 0