# 小谈 UT

# 什么是 UT?

UT(Unit Test)即单元测试

# UT 有什么价值?

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

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

我在几家公司也都没有写单元测试,因为觉得没有必要,因为代码都是自己一个人写的。其实这样想是不对的,因为交接的时候单元测试可以作为代码的文档和示例,展示如何使用特定的类和方法。而且还有一个问题当依赖的中间件越多,每次测试需要启动这些依赖,其实用 mockite 模拟数据是不需要的。

总之有无数种理由不想写 UT,作为工作不到三年的菜鸟深有体会。之前在点评工作的时候,团队的 “UT” 都集中于 RPC 的服务端。为啥带双引号?

因为 RPC 的服务端没有页面可以功能测试,部署到测试环境测试太麻烦,只能写 UT 了。在这个场景下我认为叫 “验证” 更合适,验证不等于测试。

验证往往只写主逻辑是否通过,且就一个 Case,且没有 Assert,有的是 System.out。

下图是一个测试模型:

img

图的意思就是越底层做的测试效果越好,越往上则越差。也就是说大部分公司现在做的功能测试其实是效果最差的一种测试方式。

另外,QA 界有个现场:大家都知道功能测试没技术含量,那如何使自己突出呢?

答案就是: 自动化测试

现实是没几个公司能做好自动化测试,业界做的比较好的百度算一个。那么为啥自动化测试这么难做的?在这个模型当中,越往上黑盒越大,自动化测试难度就越大。

这句话反过来就是越往下自动化测试就越好做?没错,UT 其实是最容易实现且效果最好的自动化测试。

所以在很多公司出现一种现象:QA 写 UT。

原因总结一下就两点:

  1. 开发不愿意写 UT。

  2. 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. 数据准备

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

    如图:

    说明:

    @Test: @Test 注解是 JUnit 测试框架中用于标记实际测试方法的注解。

    @Sql: @Sql 注解是 Spring Test 上下文中使用的,用于在测试执行前后自动执行 SQL 脚本来准备或清理数据。这在进行数据库相关的集成测试时非常有用,因为它可以确保每个测试都在一个已知的数据状态下执行。

    @Sql 注解的属性包括:

    • scripts :指定需要执行的 SQL 脚本路径。
    • statements :指定内联的 SQL 语句。
    • executionPhase :定义 SQL 执行的阶段,例如 BEFORE_TEST_METHODAFTER_TEST_METHOD
    • config :指定 @SqlConfig 注解,用于配置 SQL 脚本的解释方式。
  2. 执行逻辑

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

  3. 输出验证

    查看输出的内容,验证,或者写 Assert 来断言结果。

集成测试一般是调用 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 应该在它们自身的单元测试去测试。对于依赖的第三方,我们应该信任它们能正确的完成我们所预期的。这句话很难理解对不对? 下面是例子。

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

      我们应该验证的内容是:

      • dao 的 create 方法被调用了
      • 调用次数是对的
      • 调用参数也是对的
    2. 没错,只要这三个验证通过,那么这个 case 执行就是通过的。因为我们相信 dao 的 create 操作能正确的完成我们所预期的,只要我们调用了正确的次数并且参数都是对的。dao 的执行的正确性保证是在该 dao 的单元测试做的。

      在 Remote Integration Test 里面第三步验证道理是一样的,我们应该验证 RPC 接口被调用了且次数和参数都是对的,那么我们的 case 就算通过了,至于,RPC 服务端是否正确执行是它们的事情不是我们所关心的。

      Mockito 框架的 verify 接口就是做这件事情的。如果你理解了上述内容,那么你就开窍了,UT 不在变得这么难写。什么时候用单元测试,什么时候用集成测试?在本人的实践中摸索发现,对于简单的业务,比如 crud 型的瘦 service,比较适合于集成测试。
      以下情况适合于单元测试:

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

# case 细到什么程度为好?

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

我的建议是两个原则:

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

原文地址:http://lepdou.github.io/blogs/ut