单元测试用例编写流程 编写单元测试代码遵守BCDE原则 AIR原则

 

Java编程技巧之单元测试用例编写流程 https://mp.weixin.qq.com/s/hX_RIYs-nBnqVwdq5B4rhg

收藏!Java编程技巧之单元测试用例编写流程

 

Java单元测试技巧之PowerMock

https://mp.weixin.qq.com/s/LSkTvpsTnBmdOB5nihkxng

高德的技术大佬向老师在谈论方法论时说到:“复杂的问题要简单化,简单的问题要深入化。”
这句话让我感触颇深,这何尝不是一套编写代码的方法——把一个复杂逻辑拆分为许多简单逻辑,然后把每一个简单逻辑进行深入实现,最后把这些简单逻辑整合为复杂逻辑,总结为八字真言即是“化繁为简,由简入繁”。
编写Java单元测试用例,其实就是把“复杂的问题要简单化”——即把一段复杂的代码拆解成一系列简单的单元测试用例;写好Java单元测试用例,其实就是把“简单的问题要深入化”——即学习一套方法、总结一套模式并应用到实践中。这里,作者根据日常的工作经验,总结了一些Java单元测试技巧,以供大家交流和学习。
 
 
1 《Java开发手册》规范
【强制】好的单元测试必须遵守AIR原则。说明:单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
  • A:Automatic(自动化)

  • I:Independent(独立性)

  • R:Repeatable(可重复)


【强制】单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证。
【强制】单元测试是可以重复执行的,不能受到外界环境的影响。
说明:单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
正例:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring 这样的DI框架注入一个本地(内存)实现或者Mock实现。
【推荐】编写单元测试代码遵守BCDE原则,以保证被测试模块的交付质量。
  • B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。

  • C:Correct,正确的输入,并得到预期的结果。

  • D:Design,与设计文档相结合,来编写单元测试。

  • E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。


2  为什么要使用Mock?
根据网络相关资料,总结观点如下:
Mock可以用来解除外部服务依赖,从而保证了测试用例的独立性
现在的互联网软件系统,通常采用了分布式部署的微服务,为了单元测试某一服务而准备其它服务,存在极大的依耐性和不可行性。
Mock可以减少全链路测试数据准备,从而提高了编写测试用例的速度
传统的集成测试,需要准备全链路的测试数据,可能某些环节并不是你所熟悉的。最后,耗费了大量的时间和经历,并不一定得到你想要的结果。现在的单元测试,只需要模拟上游的输入数据,并验证给下游的输出数据,编写测试用例并进行测试的速度可以提高很多倍。
Mock可以模拟一些非正常的流程,从而保证了测试用例的代码覆盖率
根据单元测试的BCDE原则,需要进行边界值测试(Border)和强制错误信息输入(Error),这样有助于覆盖整个代码逻辑。在实际系统中,很难去构造这些边界值,也能难去触发这些错误信息。而Mock从根本上解决了这个问题:想要什么样的边界值,只需要进行Mock;想要什么样的错误信息,也只需要进行Mock。
Mock可以不用加载项目环境配置,从而保证了测试用例的执行速度
在进行集成测试时,我们需要加载项目的所有环境配置,启动项目依赖的所有服务接口。往往执行一个测试用例,需要几分钟乃至几十分钟。采用Mock实现的测试用例,不用加载项目环境配置,也不依赖其它服务接口,执行速度往往在几秒之内,大大地提高了单元测试的执行速度。
3  单元测试与集成测试的区别
在实际工作中,不少同学用集成测试代替了单元测试,或者认为集成测试就是单元测试。这里,总结为了单元测试与集成测试的区别:
测试对象不同
单元测试对象是实现了具体功能的程序单元,集成测试对象是概要设计规划中的模块及模块间的组合。
测试方法不同
单元测试中的主要方法是基于代码的白盒测试,集成测试中主要使用基于功能的黑盒测试。
测试时间不同
集成测试要晚于单元测试。
测试内容不同
单元测试主要是模块内程序的逻辑、功能、参数传递、变量引用、出错处理及需求和设计中具体要求方面的测试;而集成测试主要验证各个接口、接口之间的数据传递关系,及模块组合后能否达到预期效果。
 
 
 二  测试用例编写流程
通过上一章编写Java类单元测试用例的实践,可以总结出以下Java类单元测试用例的编写流程:

图片

单元测试用例编写流程
上面一共有3个测试用例,这里仅以测试用例testCreateUserWithNew(测试:创建用户-新)为例说明。
1  定义对象阶段
第1步是定义对象阶段,主要包括定义被测对象、模拟依赖对象(类成员)、注入依赖对象(类成员)3大部分。
定义被测对象
在编写单元测试时,首先需要定义被测对象,或直接初始化、或通过Spy包装……其实,就是把被测试服务类进行实例化。
/** 定义被测对象 *//** 用户服务 */@InjectMocksprivate UserService userService;

模拟依赖对象(类成员)

在一个服务类中,我们定义了一些类成员对象——服务(Service)、数据访问对象(DAO)、参数(Value)等。在Spring框架中,这些类成员对象通过@Autowired、@Value等方式注入,它们可能涉及复杂的环境配置、依赖第三方接口服务……但是,在单元测试中,为了解除对这些类成员对象的依赖,我们需要对这些类成员对象进行模拟。
/** 模拟依赖对象 *//** 用户DAO */@Mockprivate UserDAO userDAO;/** 标识生成器 */@Mockprivate IdGenerator idGenerator;

注入依赖对象(类成员)
当模拟完这些类成员对象后,我们需要把这些类成员对象注入到被测试类的实例中。以便在调用被测试方法时,可能使用这些类成员对象,而不至于抛出空指针异常。
/** 定义被测对象 *//** 用户服务 */@InjectMocksprivate UserService userService;/** * 在测试之前 */@Beforepublic void beforeTest() {    // 注入依赖对象    Whitebox.setInternalState(userService, "canModify", Boolean.TRUE);}

2  模拟方法阶段

第2步是模拟方法阶段,主要包括模拟依赖对象(参数或返回值)、模拟依赖方法2大部分。
模拟依赖对象(参数或返回值)
通常,在调用一个方法时,需要先指定方法的参数,然后获取到方法的返回值。所以,在模拟方法之前,需要先模拟该方法的参数和返回值。
Long userId = 1L;

模拟依赖方法

在模拟完依赖的参数和返回值后,就可以利用Mockito和PowerMock的功能,进行依赖方法的模拟。如果依赖对象还有方法调用,还需要模拟这些依赖对象的方法。
// 模拟依赖方法// 模拟依赖方法: userDAO.getByNameMockito.doReturn(null).when(userDAO).getIdByName(Mockito.anyString());// 模拟依赖方法: idGenerator.nextMockito.doReturn(userId).when(idGenerator).next();

3  调用方法阶段
第3步是调用方法阶段,主要包括模拟依赖对象(参数)、调用被测方法、验证参数对象(返回值)3步。
模拟依赖对象(参数)
在调用被测方法之前,需要模拟被测方法的参数。如果这些参数还有方法调用,还需要模拟这些参数的方法。
String text = ResourceHelper.getResourceAsString(getClass(), "userCreateVO.json");UserVO userCreate = JSON.parseObject(text, UserVO.class);

调用被测方法

在准备好参数对象后,就可以调用被测试方法了。如果被测试方法有返回值,需要定义变量接收返回值;如果被测试方法要抛出异常,需要指定期望的异常。
userService.createUser(userCreate)

验证数据对象(返回值)
在调用被测试方法后,如果被测试方法有返回值,需要验证这个返回值是否符合预期;如果被测试方法要抛出异常,需要验证这个异常是否满足要求。
Assert.assertEquals("用户标识不一致", userId, userService.createUser(userCreate));

4  验证方法阶段
第4步是验证方法阶段,主要包括验证依赖方法、验证数据对象(参数)、验证依赖对象3步。
验证依赖方法
作为一个完整的测试用例,需要对每一个模拟的依赖方法调用进行验证。
// 验证依赖方法// 验证依赖方法: userDAO.getByNameMockito.verify(userDAO).getIdByName(userCreate.getName());// 验证依赖方法: idGenerator.nextMockito.verify(idGenerator).next();// 验证依赖方法: userDAO.createArgumentCaptor userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);Mockito.verify(userDAO).create(userCreateCaptor.capture());

验证数据对象(参数)

对应一些模拟的依赖方法,有些参数对象是被测试方法内部生成的。为了验证代码逻辑的正确性,就需要对这些参数对象进行验证,看这些参数对象值是否符合预期。
text = ResourceHelper.getResourceAsString(getClass(), "userCreateDO.json");Assert.assertEquals("用户创建不一致", text, JSON.toJSONString(userCreateCaptor.getValue()));

验证依赖对象

作为一个完整的测试用例,应该保证每一个模拟的依赖方法调用都进行了验证。正好,Mockito提供了一套方法,用于验证模拟对象所有方法调用都得到了验证。
// 验证依赖对象Mockito.verifyNoMoreInteractions(idGenerator, userDAO);



 
 
 

 

posted @ 2021-05-13 09:48  papering  阅读(1686)  评论(0编辑  收藏  举报