KOK球盘体育
当前位置: KOK球盘体育 > 单元作文 >

如何写出高质量单元测试

时间:2020-03-10 22:29来源:未知 作者:admin 点击:
KOK球盘体育

当前网址:http://www.wassei.com/danyuanzuowen/2020/0310/776.html

  随着系统便来越来越复杂,为了便于业务的持续快速迭代和后期维护,庞大的系统最终都会按业务拆分成一些垂直子系统,子系统之间通过接口进行相互调用,各子系统的具体实现对其它子系统不可见,只需制定清晰的接口规范,这就是设计模式中依赖倒置原则。随着子系统的不断增多,集成测试的成本变得越来越高,测试一个业务流程需要部署所有相关的子系统,搭建调试环境的成本很高。优秀的开发实践是在联调/提测之前开发对本模块的修改进行充分的单元测试,然后相互交互的系统两两联调,再全流程联调,最后测试、发布上线。

  单元测试作为最前置的一个节点,如果问题都能在这个环节发现,对需求迭代的效率提升是非常有帮助的,因为在越后置的节点发现问题修改起来代价越大。最理想的情况是,所有修改的模块一旦通过单元测试,只要各子系统之间的接口规约定制的足够清晰,在集成测试及之后的环节就不应该再出现代码层面的问题,这对单元测试的质量有着非常高的要求。

  业界比较有代表性的java测试框架是JUnit和testNG,我所在的技术团队使用testNG,下面的一些示例会居于testNG,本文的主题是介绍一些单元测试的方法和思路,不会比较这两款框架孰优孰劣。

  比较常见的做法是,单测数据直接使用单元测试库(可能是测试库或开发专用库)中的数据,但是这样做的弊端是开发/测试库库是团队公用的,数据有可能被其它人修改,会导致测试结果不稳定断言失败。所以更推荐的做法是构造自己的测试数据,在测试用例中通过调用插入数据的接口把测试数据插入到单元测试库。

  但有时测试的数据模型可能字段很多比较复杂,这时人工造数据会比较费时,这里推荐一个podam框架,它可以给某些指定的类自动生成对象,并且自动给对象中的字段生成一些随机值,只要给这些字段添加一个特定的注解:

  @PodamStringValue:作用在String字段上,该注解会让字段自动设置上一串随机字符,同时还可以增加一些限制,比如可以设置length属性,设置该字符串的最大长度,这在大部分情况下是非常有用的,比如有写场景下你要把数据插入到DB中,而DB中的字段大部分是有长度限制的,这这里限制长度之后,数据插入数据库就不会报错。

  @PodamCollection:作用在集合字段中,nbrElements指定集合中元素个数,框架会自动生成nbrElements个元素的集合设置到字段中,collectionElementStrategy属性指定元素生成策略,也是要指定一个AttributeStrategy子类,如果字段指定了泛型,这个属性可以忽略,像下面这样:

  给类型字段设置好注解之后,调用PodamFactory的一个方法就可以自动生成对象了:

  此外还可以很方便的自定义类型生成器,只需实现一个TypeManufacturer接口并注册到框架中,就可以实现自定义的数据生成逻辑,比如BigDecimal一般代表一个浮点数,但是由于它不是一个基本类型,所以无法通过现有注解生成BigDecimal类型的数据。实现方式是自定义一个注解,设置一些基本配置,通过这些属性可以指定数值范围,可以数值精度:

  这样只要给BigDecimal字段添加PodamBigDecimalValue注解,就可以自动随机生成BigDecimal值了。由于podam生成的数据是随机的,这样还能消除认造数据的一些盲区,因为每次生成的测试数据都不同,这对于验证代码的健壮性也是非常有帮助的。数据生成之后可以调用程序的dao接口直接把数据插入到DB或缓存中。

  执行测试代码,就是执行一边要测试的代码,这里要求被测试的代码是易测试的,要测试的逻辑最好是能够封装成一些独立的接口,接口不要太粗尽量细一点,尽量满足设计模式的单一职责原则,这会让代码变的更易测试。

  最简单的结果就是所测试的接口的直接返回结果,根据测试场景断言返回值应该是什么,如果不等于预测的值则说明服务逻辑有问题。还可能会有一部分执行结果是隐藏在服务实现中的,比如你的服务在某些场景写会插入一些数据到数据库中,那么在预测结果时,需要通过dao查询DB中是否存在该数据,并且断言数据的值等于预测的值,如DB中不存在数据或数据值和预测的不一样,则也说明服务逻辑有问题。记住没有断言的单测是没有灵魂的,如果执行的单测连被测服务的结果都不敢预测,那这个单测没有任何意义,我还见过有些单测所有的单测代码执行都放到一个try-catch块中,所有异常都被catch块吞掉,这纯属掩耳盗铃,浪费时间。

  测试结束之前要清理测试过程中产生的数据,这样做的意义是,单测是一个非常高频的动作,就拿我所在的技术团队来说,整个后端技术团队每天大概会执行好几万次单元测试,如果每个单测都最终插到数据库(缓存)中,库中会有大量的垃圾数据,DB容量甚至会有撑爆的风险。所以每次单测执行完之后数据最好是回滚。

  对于DB数据,直接让测试类继承testNG的AbstractTransactionalTestNGSpringContextTests类就可以了,该基类带@Transactional注解会自动给继承该类的单测套上声明式事务,而且该事务不会提交,单测执行完之后会事务会自动回滚,测试数据不会落库。这里会带来一些副作用,如果使用了mybatis框架,由于事务缓存的作用在某些情况下会产生问题,比如使用了oracle的sequence,在代码中执行了超过一次的sequence的nextVal,因为单测被套上了事务,执行这段逻辑的时候后面不管查多少次nextVal,查出来的值跟第一次的值是一样的,因为后面几次查询都被缓存拦住了,这样会出现一些奇怪的问题,好在技术团队基本上都已去oracle化了,基本上不会用到oracle sequence。

  对于缓存数据,在单测的结尾调用缓存删除接口把服务中添加进缓存的数据删掉。

  一个单测写好之后,它的执行结果应该是高度稳定可靠的,不管什么时候执行,执行多少次,每次执行都应该全部执行成功,而不能是看天吃饭,时而成功时而失败,这样会浪费大量时间排查执行问题,反而会加重单测维护者的负担。可能对单测执行稳定造成影响的,主要来自两方面:1.单测数据变更;

  对于单测数据,上面总结过了最好是每个单测造数据给自己用,这样能解决单测数据的稳定性问题,但是某些场景下还是会被数据库中的数据变更所影响。比如有个case是要查询的最近的10个订单,或者是查询总共的订单数量,一旦测试库新增了订单,那么就可能会导致测试结果断言失败。要解决这个问题有种思路是,在单测过程中把DB换掉,换成类似于hsqldb这种内存数据库,这是一款纯Java编写的免费数据库,并且支持Memory-Only模型,所以每个单测使用独立的数据库就有了可能,Memory-Only模型数据不会持久化跑完即销毁,由于库都是独享的,所以没有任何人会影响到库里的数据,可以解决这个,替换DB的过程也很简单,在test的resources下加一个入口xml,在xml中增加hsqldb相关的配置:

  在rds_hsql.script中添加建表sql,每次单测执行时都会读这个文件建表。

  2.在处理mybatis的keyColumn功能时,字段名称是大小写敏感的且只能用大写的,比如字段名称是id要改成大写的ID否则会报错,这应该是hsqldb客户端的bug。

  跟oracle兼容的问题比较多,因为已经去oracle化了,这里就不一一列举,总之如果没有使用oracle并且可以容忍上面这些问题,hsqldb还是可以尝试使用的。

  远程服务也是导致单测挂掉的一大元凶,由于单元测试的目标时测试本应用本模块内的代码,而不是测远程服务,远程服务测试应该由远程服务的单元测试和联调/继承测试负责。所以单测测试的边界只能包含本应用内的代码,依赖的服务无论出现任何问题都不应该影响单测的执行。这里可以使用mock框架把依赖的远程服务mock掉,业界比较流行的开源的mock框架有EasyMock和Mockito,基本用法很简单,比如使用Mockito,可以通过InjectMocks和Mock注解,mock掉远程服务并且把mock后的服务注入到依赖bean中替换远程服务,在执行单测的时候会执行到mock后的代码而不会调用远程服务,这样就消除了远程服务对单测的影响。mock框架的详细的用法可以查看相关的文档。

  系统越是复杂,单元测试对保障代码质量越重要,好的单测对代码维护也会带来帮助,修改老代码之后只要跑一遍单测就知道改得有没有问题,会给代码维护者增添信心。编写单元测试所占的时间比重也越来越大,我们现在一个需求做下来,单元测试代码数量已经快接近业务代码数量,有些服务的单测代码数量甚至已经超过了业务代码的数量。在我们的任务迭代流程中,单元测试节点已经固化到提测前的必执行check项,单测执行失败的任务是无法提交测试的,磨刀不误砍柴工,这些小投入给我们带来了更多的价值,让我们一起愉快的写单测吧。

------分隔线----------------------------