Mike Cohn 在十几年前曾经提出过著名的“测试金字塔”理论,将测试划分为三个层次。从上到下分别是:UI 测试、服务测试和单元测试。它们累加在一起,就像一个金字塔一样。
今天我们只说单元测试。单元测试中最麻烦的不确定因素就是各中间件,常见于数据库、缓存、MQ,这些中间件的历史数据或单元测试时交叉并发产生的数据(如多个人在跑同一个单元测试或是同时跑不同单元测试但产生了相互影响的数据)都是单元测试所要杜绝的。我们对应大概有三个方案:
使用真实环境,执行自动清理 以DBUtil为代表,这类工具会使用真实的中间件,但在测试完成后执行自动清理工作,还原测试中变更的数据,这一方案会影响单元测试的独立性,测试时准备外部环境,对持续集成中的自动化测试会有比较大干扰,如无必要最好不要使用。
使用模拟环境 以Mockito为例,这类工具会要求定义Mock的类型及对应方法的期望返回,核心的代码示例如下:
// 定义要Mock的对象
private UserDao userDao=mock(UserDao.class);
// 定义方法及模拟的返回
when(userDao.findAll()).thenReturn(10);
// 测试
assertEquals(10, userDao.findAll());
这一方案解决了上一方案的问题,使单元测试更为内聚,是比较理想的手段,它的不足在于需要针对性地定义Mock代码,对复杂逻辑而言不是很友好,更为严重的是它无法发现由中间件引发的数据问题,例如在一段代码中由于开发失误连续调用了两次相同的插入数据命令,实际环境下要返回主键冲突,但Mock下就不容易发现。
使用内嵌的可替代环境 比如线上是MySQL,测试时使用H2,Redis缓存测试时可使用embedded-redis等,这一方案的好处是测试完全不用加任何Mock代码,非常干净,同时又可以比较好地模拟真实的环境,缺点在于有些中间件没有相应的内嵌版本。
笔者在单元测试实战过程中,也踩过一些坑一些经验,分享一些tips:
单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。test目录需要手动创建,ALT+Ctrl+T创建单元测试。
如需java bean转Json的话,使用插件 java-bean-to-json。 json数据可以直接存储在file文件中,其他的测试类可以复用。单元测试上下文获取登录信息要通用。
避免单元测试类中过长的set方法,精简代码。没有复用性的数据放在单元测试内部,不要干扰他人。
编写单元测试时, 仅仅需要关注单个类就可以。不需要关注类的上下文,例如数据库服务, Web 服务等组件。依赖的bean使用mockbean的方式注入。
Service方法里面调用方法,被调用方可以不用写。保证调用方方法覆盖完全即可。
在项目提测前完成单元测试,不建议项目发布后补充单元测试用例。单元测试循序渐进推动,提升单元测试覆盖率(单元测试的评估基准主要是逻辑覆盖率)。如果覆盖率比较低或者测试结构过于复杂,应考虑优化代码,使逻辑简明清晰。
为了更方便地进行单元测试,业务代码应避免以下情况:构造方法中做的事情过多。(比如一些极端的调用数据库查询出的数据来set值)。存在过多的外部依赖。存在过多的条件语句。
落地点:纯Mock单元测试,集成测试、端到端测试先放弃,确保单元测试能落地(单元测试>>集成测试>>端到端测试)。单元测试是不依赖spring容器,也不依赖于其他的环境。@SpringBootTest和@Autowared注解会启动spring容器,导致单元测试时间增长。
Service层void方法,可以用DAO层方法调用次数断言。
单元测试mock生成插件(TestMe or Squaretest),解放生产力。
总结一下,单元测试的要点是尽可能做到:目标功能单一 一个测试只针对一到几个功能。减少数据干扰 这是上文着重强调的点。降低环境依赖 要求可以在开发人员自己环境执行,也必须可以在CI(持续集成,后续会有介绍)下执行。编写简单 单元测试规范的项目其测试点会覆盖所有的核心方法,其工作量很大,所以必须要简单化可修改。落地的重要一点还要可量化:比如发包的时候sonar自动扫描,单元测试覆盖率不到X%,构建失败。只有落地好,代码质量和系统质量才能上升一个层级。
https://gudaoxuri.gitbook.io/microservices-architecture/wei-fu-wu-hua-zhi-kai-fa-yu-ce-shi/unit-test
极客时间《遗留系统现代化实战》
精进自省:开启重学系列,任何技术都值得重新学习一遍。保持开放心态,照顾好自己身体,充分准备,像狗熊一样过冬。