如何定义单元
对于单元测试中的单元, 不同的人有不同的看法: 可以理解为一个方法, 可以理解为一个完整的接口实现, 也可以理解为一个完整的功能模块或者是多个功能模块的一个耦合.
根据以往的单元测试经验, 在设计单元测试用例时, 当针对方法级别展开单元测试时, 重点关注的是方法的底层逻辑; 当针对的是模块时, 针对的是实际的业务逻辑实现; 当针对整合后的模块进行测试时, 一般称之为集成测试.
不管是单元测试还是集成测试, 都可以统一的理解为单元测试. 因为他们的本质都是对方法或接口的一种测试形式, 只是所处的阶段不一样罢了.
1. 集成测试应该由谁编写
在我们的实际工作中, 研发人员在提交代码之前, 会设计一些 "冒烟测试" 级别集成测试用例. 等到整个功能开发完成后, 测试人员会根据业务需求和设计的测试用例, 来进行整体的集成测试用例的编写, 执行, 失败用例分析, 以及代码的调式和问题代码的定位等工作.
2. 集成测试用例
业务相关的测试主要是通过 spring-test 来进行集成测试, 基本的测试结构为先定义一个基类用来初始化被测试类.
测试基类定义结构如下:
- @RunWith(SpringJUnit4ClassRunner.class)
- ContextConfiguration(locations = {"classpath:./spring/applicationContext.xml"})
- @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
- public class BaseSpringJunitTest {
- @Autowired
- protected BusinessRelatedServiceImpl businessRelatedService;
- }
业务相关的测试类定义如下格式:
- public class BusinessRelatedServiceImplDomainTest extends BaseSpringJunitTest {
- @Test
- public void testScenario1 (){
- new Thread(new DOSAutoTest("testScenario1")).start();
- Thread.sleep(1000*60*1);
- String requestJson=""// 测试入参;
- RequestPojo request=( RequestPojo )JSONUtils.jsonToBean(requestJson,RequestPojo .class);
- ResponsePojo response= businessRelatedService.businessRelatedMethod(ResponsePojo );
- // 业务相关的 assert 区域
- }
- }
3. 如何解决下游系统依赖
businessRelatedMethod 方法在处理业务逻辑的过程中需要调用下游 JSF(Jingdong Service Framework, 完全自主研发的高性能 RPC 服务框架) 提供的订单接口 (OrderverExportService), 并根据入参中的订单编号获取订单的详细信息 (ResultPojo getOrderInfoById (long orderId)).
那么如何获取下游 JSF 接口的返回正确数据就变成了一个比较重要的问题. 如果是在功能测试或者联调测试阶段, 可以由下游测试人员来提供数据. 不过这样沟通和测试成本较高, 无法满足业务快速上线和变化的要求, 尤其在集成测试阶段这个问题就变得尤为明显, 因为下游数据对于上游来说是不可控的. 这样 mock 下游数据就变得尤为紧急和重要.
4. Mock 框架的选择
在整个 java 生态圈中, 支持 mock 的开源框架还是比较多的, 比如常用的 mockito,powermock,easymock 和 jmockit 等开源框架. 这些框架在 mock 方面都具有比较强大的功能与比较广泛的使用量. 但是这些框架都具有一个相同的缺点, 那就是需要或多或少的编码工作来 mock 所需要的接口返回数据.
在设计 mock 框架的时候, 我们考虑到尽量让写单元测试的人员或研发人员少编码或不编码, 来获取不同的业务场景所需要的测试数据.
Mock 框架 第一版
该版本的 mock 框架的整体思想为: 结合 JSF 的特性, Override 所有下游接口的方法, 然后将实现下游接口的应用部署到测试环境, 发布一个有别与真实下游接口的服务, 在接口调用的时候, 通过不同的 JSF 接口别名来进行区分. Mock 的数据存储在数据库中.
该框架类调用关系:
Mock 接口的具体实现:
- public class OrderverExportServiceImp extends OrderverExportServiceAdapter {
- @Resource
- private OrderverMapper orderverMapper;
- @Override
- public ResultPojo getOrderInfoById (long orderId) {
- OrderverPojo orderverMock=orderverMapper.getOrderId(new Long(orderId).toString());
- ResultPojo result=new ResultPojo ();
- result.setFiled1(null);
- result.setFiled1(0);
- result.setFiled2(null);
- result.setFiled3(null);
- result.setResult(true);
- ...//mock 需要的数据
- result.setReturnObject(orderver);
- return result;
- }
- }
Mock 服务发布完后的效果:
在集成测试阶段, 只需要修改该接口的 JSF 别名, 就可以实现该接口的 mock 调用.
- protocol="jsf" timeout="${timeout}"
- alias="${alias}" retries="2" serialization="hessian">
- alias=orderver_mock
该框架的优缺点
优点:
做集成测试用例设计时, 不用编写代码, 只需要维护测试场景所需要的返回数据;
该框架不仅可以用在集成测试中, 在下游接口无变更的前提下, 同时还可以用在后续系统测试与联调测试阶段.
缺点:
mock 服务的发布依赖于服务器与数据库, 当依赖的服务器或数据库出现跌机情况时, 该 mock 服务不用;
该框架的维护成本比较大, 当下游依赖的接口较多时, 所有的服务包含的方法均需要进行 override;
当下游的接口定义发生变化时比如新增接口方法, 该 mock 服务需要重新 override 该新增的方法并且需要重新打包部署;
下游接口方法的数据结构发生变化时, 存储数据的数据表结构需要做相应的调整, 对于业务变化较快的系统, 这种类型的改动频率还是较高.
Mock 框架 第二版
为了解决上述 mock 框架依赖服务器与数据库的问题, 我们又做了第二次尝试. 将 mock 框架设计为 jar 包的形式, 提供给程序来调用. 在下游接口的实现方式上第二版与第一版保持不变, 同时业务数据不放数据库, 而是将业务数据放到文件中. 变化的点为接口调用上需要将对应的 jsf:comsumer 节点替换为对应的实际 mock 的实现类.
Mock 接口的实现:
- @Service("orderverExportService")
- public class OrderverExportServiceMock extends OrderverExportServiceAdapter {
- @Override
- public ResultPojo getOrderInfoById(long orderId) {
- ResultPojo result=new ResultPojo ();
- result.setFiled1(null);
- result.setFiled1(0);
- result.setFiled2(null);
- result.setFiled3(null);
- result.setResult(true);
- ...//mock 需要的数据
- result.setReturnObject(orderver);
- return result;
- }
- }
Mock 接口调用配置:
-->
该框架的优缺点
优点:
做集成测试用例设计时, 不用编写代码, 只需要维护测试场景所需要的返回数据;
相比较第一个版本, 该版本在执行效率上有了较大的提升, 因为 mock 类的加载是走的本地 Spring 配置文件, 同时数据加载也是走的本地文件;
无需再依赖于服务器部署和数据库依赖.
缺点:
该框架的维护成本比较大, 当下游依赖的接口较多时, 所有的服务包含的方法均需要进行 override;
当下游的接口定义发生变化时比如新增接口方法, 该 mock 服务需要重新 override 该新增的方法并且需要重新打包, 然后上传到 maven 仓库;
下游接口方法的数据结构发生变化时, 对于业务变化较快的系统, 这种类型的改动频率还是较高.
Mock 框架 第三版
随着需要 mock 的接口变的越来越庞大, 以上两种 mock 框架的实现的缺点就变的越来越突出. 该框架可以说从根本上解决了上述框架实现的问题. 因为该框架充分利用了 JDK 的动态代理, 反射机制以及 JSF 提供的高级特性来实现我们的 mock 框架. 框架维护任务可以做到无需做更多的针对接口的编码任务. 测试人员只需要将重点放在测试数据的准备上.
框架整体调用时序图:
框架的核心类图:
其中 DOSAutoTest 类用来启动和发布 JSF 的 mock 接口, JSFMock 通过动态代理的方式, 实现下游接口的 mock 功能并根据测试场景获取对应的 mock 数据.
其中, mock 的数据以 json 格式存储在 mock 框架项目工程的指定目录下.
该框架解决的问题
省去了利用第三方 mock 框架如 jmockit,mockito,powermock 时, 需要在单元测试或集成测试类中写 mock 代码的麻烦;
该框架模拟数据返回时, 完全的模拟了接口之间的调用关系;
测试人员或研发人员在利用该框架 mock 数据时, 无需额外的代码, 就可以实现 mock 数据的返回;
在模拟下游数据返回时, 发布的 mock 接口调用完成后就自行销毁, 无需额外服务器进行部署与维护.
在进行接口 mock 时, 无需在 mock 框架中添加相关的接口 maven 依赖.
单元测试展开方式
1. 单元测试应该由谁编写
单元测试由谁编写? 针对这个问题, 大家在网上会找到不同的观点:
一个观点是, 谁写代码, 谁自己写单元测试. 当然, 有的结对编程里面, 也有相互写的, 不过, 这个过程中, 两个人是共同完成的代码. 也不违反谁写代码谁写单元测试的原则.
另一个观点是单元测试应该由其它的研发人员或测试人员来进行编写, 理由大概可以理解为对于非代码编写人员来说, 在设计单元测试用例的时候, 对应的是一个黑盒. 在这样的背景下, 设计出来的用例覆盖程度更高.
2. 单元测试的行业现状
如果研发来负责单元测试的编写, 很多时候研发人员都不编写单元测试. 研发人员不编写单元测试的原因其实也是比较容易理解的, 因为编写单元测试用例工作太耗时. 有时候研发的经理或项目的业务方会认为单元测试用例会减缓项目的整体进度. 有时候甚至整个公司层面都不认可花费大量的时间在单元测试上是合理的, 尤其是在项目周期紧张和业务变动较大的项目上. 因为单元测试从一定程度上来说确实增加的研发人员的编码量, 同时还会增加代码的维护成本.
如果测试来负责单元测试的编写, 目前的现状是测试人员需要时间理解代码, 写单元测试的时间会变长. 有代码修改之后, 在项目的测试压力之下, 有的测试人员, 就选择不维护单元测试, 而选择赶紧完成传统的手工测试.
3. 单元测试用例自动生成
人工编写测试用例成本增加, 那么我们考虑是否可以通过自动生成的方式来实现单元测试呢? EvoSuite 是由 Sheffield 等大学联合开发的一种开源工具, 用于自动生成测试用例集, 生成的测试用例均符合 Junit 的标准, 可直接在 Junit 中运行.
对于非业务相关的模块, 在单元测试的实践中, 就可以直接使用上述工具来自动生成单元测试代码. 虽然该工具只是辅助测试, 并不能完全取代人工, 测试用例的正确与否还需人工判断, 但是通过使用此自动测试工具能够在保证代码覆盖率的前提下极大地提高测试人员的开发效率.
下面来详细介绍如何使用该工具生成单元测试用例以及如何检查单元用例的正确性.
EvoSuite 为 Maven 项目提供了一个插件, 该插件的具体配置如下所示:
- org.evosuite.plugins
- evosuite-maven-plugin
- ${evosuiteVersion}
- prepare
- process-test-classes
除了需要配置上述 plugin 外, maven 还需要做如下的配置:
- org.evosuite
- evosuite-standalone-runtime
- ${evosuiteVersion}
- test
- org.apache.maven.plugins
- maven-surefire-plugin
- ${maven-surefire-plugin-version}
- true
- true
- false
- listener
- org.evosuite.runtime.InitializingListener
上述 plugin 主要是用来混合执行手动设计的单元测试用例和使用 EvoSuite 自动生成的单元测试用例.
以上 EvoSuite 所需的 plugin 和 maven 依赖配置完成之后, 就可以使用 maven 命令来自动生成单元测试用例并执行了.
mvn -DmemoryInMB=2000 -Dcores=2 evosuite:generate evosuite:export test
生成测试用例后, 可以通过人工排查生成测试用例的正确性.
写在最后
不管是研发还是测试负责集成或单元测试, 选取适合自身项目的 mock 框架, 一方面可以缩短测试代码的编写时间, 另一方面可以加速测试代码的执行效率, 同时又可以降低测试代码的维护成本. 不管是行业中通用的 mock 框架还是定制化的框架, 都可以广泛的应用的测试中.
因为做 mock 框架不是目的, 目的是为了能高效的设计出更多的测试覆盖场景, 来进一步提升测试效率, 保证产品质量和将测试人员从繁重的手工测试中得以解放.
当单元测试代码已经准备完毕, 如何才能发挥测试代码的作用以及如何评价测试代码的效率和做单元测试的投入产出比如何来衡量等等这些问题, 将在后续的文章中给大家一一解答.
来源: http://zhuanlan.51cto.com/art/201809/583467.htm