小谈 Java 单元测试

摘要: 原文可阅读 http://www.iocoder.cn/Fight/A-little-bit-about-Java-unit-testing 「lepdou」欢迎转载,保留摘要,谢谢!

  • 什么是UT?
  • UT有什么价值?
  • Unit Test & Intergration Test
    • Local Integration Test
    • Remote Integration Test
    • Unit Test
  • case细到什么程度为好?
  • 总结

什么是UT?

UT(Unit Test)即单元测试

UT有什么价值?

大部分的开发都不喜欢写UT,原因无非以下几点:

  1. 产品经理天天催进度,哪有时间写UT
  2. UT是测试自己的代码,自测?那要QA何用?
  3. 自测能测出bug?都是基于自身思维,就像考试做完第一遍,第二遍检查一样,基本检查不出什么东西
  4. UT维护成本太高,投入产出比太低
  5. 不会写UT

总之有无数种理由不想写UT,作为工作不到三年的菜鸟深有体会。之前在点评工作的时候,团队的“UT”都集中于RPC的服务端。为啥带双引号? 因为RPC的服务端没有页面可以功能测试,部署到测试环境测试太麻烦,只能写UT了。在这个场景下我认为叫“验证”更合适,验证不等于测试。 验证往往只写主逻辑是否通过,且就一个Case,且没有Assert,有的是System.out。

本人实习的时候做测试的,那时候知道一个测试模型。如下图:

(图一)

图的意思就是越底层做的测试效果越好,越往上则越差。也就是说大部分公司现在做的功能测试其实是效果最差的一种测试方式。 另外,QA界有个现场:大家都知道功能测试没技术含量,那如何使自己突出呢?答案就是:自动化测试。现实是没几个公司能做好自动化测试, 业界做的比较好的百度算一个。那么为啥自动化测试这么难做的?在这个模型当中,越往上黑盒越大,自动化测试难度就越大。 这句话反过来就是越往下自动化测试就越好做?没错,UT其实是最容易实现且效果最好的自动化测试。 所以在很多公司出现一种现场:QA写UT。 原因总结一下就两点:开发不愿意写UT,QA想自动化测试解放自己。 以上的模型只是理论上说明UT具有巨大的价值,但是真的如此么?我只想说,只有真正尝到UT的好处的甜头才会意识到UT的价值。

Unit Test & Intergration Test

单元测试和集成测试的界线我相信大部分开发也是不清晰的。个人理解单元测试针对于一块业务逻辑最小的单元,太抽象。物理上可以简单理解为一个类的方法, 可以是public方法也可以是private方法。一个单元测试不应该包含外部依赖的逻辑,反之就是集成测试了。 问题的核心就在于此。一个service的一个接口实现可能依赖很多第三方:1.本地其它的service 2.dao调用 3.rpc调用 4.微服务调用。如下图:

图二

也就是说你的单元测试,真正调用了外部依赖那就是集成测试。这其实很常见对不?我们先说这种情况下如何集成测试。

Local Integration Test

本地集成测试也就是说不依赖与其他进程。包括:service依赖其他本地service或者dao的情况。在讲述如何集成测试之前,我们先理一下测试模型,测试主要包含三块内容:1.数据准备 2.执行逻辑 3.输出验证。

第一步:数据准备

在本地集成测试里,数据来源基本上来自于dao,dao来自于sql。也就是在执行一个case之前,执行一些sql脚本,数据库则使用h2这类memory database, 切记不要依赖公司测试环境的db。下图是使用spring-test框架的一个case,可以在case执行之前准备我们所需要的各种数据, 另外在执行完case之后,执行clean.sql脚本来清理脏数据。这里也说明一个case的执行环境是完全独立的,case之间互不干扰,这很重要。

(图三)

第二步:执行逻辑最简单,就是调用一下我们测试的方法即可

第三步:验证

集成测试一般是调用service,或者dao的接口验证。

举个例子:CRUD操作的集成测试

  1. 调用C接口
  2. 调用R接口,验证C成功
  3. 调用U接口
  4. 调用R接口,验证U成功
  5. 调用D接口
  6. 调用R接口,验证D成功

Remote Integration Test

假设我们一个service实现依赖某个RPC Service

第一步:数据准备

跑到别人家的数据库插几条数据?或者跟PRC Service的Owner商量好,搭一个测试环境供我们测试?有些公司还真有专门的自动化测试环境,那么即使有测试环境,那如何实现各种case场景下,第三方Service很配合的返回数据给我们?想想都蛋疼。

第二步:执行方法

假设我们成功的解决了第一步中的问题,皆大欢喜。现在来看第二步,假设我们的service里面调用了另一个RPC Service创建了很多数据,跑了无数次case,结果….RPC Service对应的数据库都是我们的脏数据,如何清理?而且他们敢随便删数据吗?想想也蛋疼。

第三步:输出验证

假设我们又愉快的解决了第二步中的问题。现在来看第三步,假设我们的方法执行最终输出是创建了一个订单,订单当然是调用订单Service接口了,那么我们如何验证订单是否成功创建了呢?或许可以调用订单Service查询订单的接口来验证。很明显大多数情况下并没有这么完美。想想也蛋疼呀。

通过以上分析,Local Integration Test是可行的,Remote Integration Test基本不可行。

那么有没有什么办法解决呢?答案就是Mock

  • 第一步:Mock RPC Service 想返回什么数据就返回什么数据
  • 第二步:还是Mock接口,想调用几次就调用几次
  • 第三步:这一步等到下面讲完单元测试就明白了

Unit Test

上面我们谈到Mock可以解决外部依赖的问题,现在有很多Mock的开源框架比如:mockito。那么问题来了,既然我们可以mock第三方远程依赖,为何不mock dao、local service呢?没错外部依赖全部mock掉,就是单元测试了。因为我们只关心所测试的方法的业务逻辑,也就是真正高内聚的逻辑单元了。如下图:

(图四)

好处如下:

  1. 没有什么数据是造不出来的,通通返回Mock的对象
  2. 代码中的异常处理代码,也可以通过mock接口,使之抛出异常
  3. 不产生任何脏数据
  4. 跑case更快了,因为不用启动整个项目,相当于Main方法

有人会说,都mock了还测试个蛋蛋。

这就是对于单元测试的理解了,单元测试应该只针对于目标方法的业务逻辑测试,dao、其它service应该在它们自身的单元测试去测试。对于依赖的第三方,我们应该信任它们能正确的完成我们所预期的。这句话很难理解对不对?

举几个例子

例子一:方法的最后是执行dao的create操作,那么该如何验证?

我们应该验证的内容是:

  1. dao的create方法被调用了
  2. 调用次数是对的
  3. 调用参数也是对的

没错,只要这三个验证通过,那么这个case执行就是通过的。因为我们相信dao的create操作能正确的完成我们所预期的,只要我们调用了正确的次数并且参数都是对的。dao的执行的正确性保证是在该dao的单元测试做的。 在Remote Integration Test里面第三步验证道理是一样的,我们应该验证RPC接口被调用了且次数和参数都是对的,那么我们的case就算通过了,至于,RPC服务端是否正确执行是它们的事情不是我们所关心的。 Mockito框架的verify接口就是做这件事情的。如果你理解了上述内容,那么你就开窍了,UT不在变得这么难写。

什么时候用单元测试,什么时候用集成测试?

在本人的实践中摸索发现,对于简单的业务,比如crud型的瘦service,比较适合于集成测试。

以下情况适合于单元测试:

  1. Util类
  2. 含有远程调用的方法
  3. 输入少,业务逻辑复杂的方法
  4. 需要异常处理的方法

case细到什么程度为好?

这个问题也是比较经典的,一个方法要是所有的路径都覆盖到,那么要写很多的case,说真的累死人。我的建议是两个原则:

  1. 核心逻辑,容易出错的逻辑一定要覆盖到
  2. 根据自己的时间。 没必要写的非常多,毕竟case维护成本很高,业务逻辑一改,case得跟着改。

总结

本人目前在从事于开源项目(Apollo(配置中心))研发,开源项目对代码质量要求相对来说高一些,UT当然是很重要的一环。刚开始也不会写UT,当然态度上也不重视UT。老大的代码UT覆盖率很高,抱着对开源负责的态度慢慢接受学习UT,到后来尝了几次甜头后,发现UT真的很实用,价值也很高,但是很遗憾UT被大部分开发所忽略。当然本人对UT的理解、实践还不够,仍需继续实践模式。

最后说一句:当开发完功能,跑完UT,你可以放心的上线了的时候,你的UT就成功了。



原文发布于微信公众号 - 芋道源码(YunaiV)

原文发表时间:2018-08-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Linyb极客之路

API设计:先思考再编码

14330
来自专栏程序员的碎碎念

π框架之ADM分层架构

目录结构 phalapi 2.X版本与第一个版本在项目目录结构上有很大的差异,更面向自动化、国际化和流行化,学习起来更易懂。(前提是你理解了composer、命...

42980
来自专栏码代码的陈同学

Spring Cloud 网关异常处理实践

有余力可以自建异常处理平台,有一套异常处理流程,有个炫酷且实用的Dashboard。

710200
来自专栏PhpZendo

事件驱动架构设计

这篇文章是 软件架构演进 一个有关 软件架构 系列文章中的一篇。这些文章,主要是我学习软件架构、对软件架构的思考及使用方法的记录。相比于这个系列的前几篇文章,本...

84720
来自专栏鸿的学习笔记

简单聊聊分布式系统的一致性问题

记得有人说过,分布式系统的所有问题归根结底都是一致性问题。前面文章提到的数据复制,分区以及事务面临的问题都是如何保证数据一致。而分布式系统不同于单机,它不...

9210
来自专栏玉树芝兰

如何用 pipenv 克隆 Python 教程代码运行环境?(含视频讲解)

咱们的 Python 教程代码已经可以免安装在线运行了。但如果你希望在本地克隆运行环境,请参考本文的步骤说明。

13230
来自专栏IT米粉

Redis常见的应用场景解析

缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis...

85080
来自专栏Java架构沉思录

Web系统权限控制如何设计

这篇文章的定位,不是宣传某个框架,仅仅之是梳理一下有关权限方面的一些想法和最近项目中的一些探索过程。 我们主要想解决一下问题。

73820
来自专栏FreeBuf

Any.Run交互式恶意软件分析沙盒服务现向公众免费开放

近日,名为Any.Run的交互式恶意软件分析沙盒服务宣布,其免费社区版本正式向公众开放。这样一来任何人只需简单的注册一个账号,就可以使用该平台实时的对某个特定文...

41460
来自专栏IT米粉

Redis常见的应用场景解析

Redis是一个key-value存储系统,现在在各种系统中的使用越来越多,大部分情况下是因为其高性能的特性,被当做缓存使用,这里介绍下Redis经常遇到的使用...

39560

扫码关注云+社区

领取腾讯云代金券