单元测试
Unit Test
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试 ,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
指导思想
如何才能写出真正有价值的单元测试,而不是单纯为了绩效中的单元测试覆盖率?
阿里 Java开发手册
|
|
实现方式
- 单层隔离
- 内部穿透
单层隔离
正常代码分层会分为controller、service、dao等,在单层隔离的思想中,是针对每一层的代码做各自的单元测试,不向下穿透。这样的写法主要是保证单层的业务逻辑固化且正确。 实践过程中,例如针对controller层编写的单元测试需要将对应controller类代码文件外部所有的调用全部mock,包括对应的内部/外部的service。其他层的代码也是如此。
这样做的优点:
- 单元测试代码极其轻量,运行速度快。由于只保证单个类内部的逻辑正确,其他全部mock,所以可以放弃中间件的mock,甚至Spring的注入都可以放弃,专注在单元测试逻辑验证的编写。这样整套单元测试代码运行完成应该也是轮秒计时,相对来讲Spring容器初始化完成可能都需要20秒。
- 真正符合了单元测试的原则,可以在断网的情况下进行运行。单层逻辑中可以屏蔽服务注册和配置管理,各种中间件的影响。
- 单元测试质量更高。针对单层逻辑的验证和断言能够更加清晰,如果要覆盖多层,可能会忽略丢失中间的各种验证环节,如果加上可能条件规模是一个笛卡尔乘积过于复杂。
缺点也是存在:
- 单元测试的代码量比较大,因为是针对每层单独编写单元测试,而且需要mock掉的外部依赖也是比较多的。
- 学习曲线相对较高,由于程序员的习惯针对单元测试是给定输入验证输出。所以没有了底层的输出,单纯验证过程逻辑要存在一个思维上的转变。
- 对于低复杂度的项目比较不友好。如果你的项目大部分都是单纯的分层之后的CRUD,那单元测试其实可验证的东西不太多。但是如果是代码当中执行了复杂逻辑,这样的写法就能够起到比较好的质量保证。
在这个项目中,最终没有采用这样的方法,而是采用了穿透的方式。项目的场景、人员组成、复杂度的实际情况,我觉得用这种方式不算很合适。
内部穿透
穿透,自然就是从顶层一直调用到底层。为什么还要加上内部二字?就是除了项目内的方法可以穿透,项目外部依赖还是要mock掉的。 实践过程中,就是单元测试针对controller层编写,但是会完整调用service、dao,最终对落地结果进行验证。
优点:
- 代码量相对较小,由于进行了穿透所以多层代码的覆盖仅需要从顶层的单元测试验证即可。
- 学习曲线低,穿透的单元测试更偏向黑盒,开发人员构造输入条件,然后从落地结果中(存储,例如数据库)验证预期结果。
缺点:
- 整体较重,启动Spring容器,中间件mock,整体单元测试运行预计需要是需要分钟级别。所以基本是要在CI的时候来执行。
技术实现
JUnit 5
The 5th major version of the programmer-friendly testing framework for Java and the JVM
与以前的 JUnit 版本不同,JUnit 5 由来自三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage:
JUnit Platform 是在 JVM 上启动测试框架的基础。 TestEngine API 此外,该平台还提供一个控制台启动器,从命令行启动平台。 JUnit Platform Suite Engine 使用平台上一个或多个测试引擎运行一个自定测试套件(test suite)。 支持 JUnit Platform 的有 流行的 IDE (IntelliJ IDEA, Eclipse, NetBeans, and Visual Studio Code) 和构建工具 (Gradle, Maven, and Ant)。
JUnit Jupiter 是用于在 JUnit 5 中编写测试和扩展的新编程模型和扩展模型的组合。 Jupiter 子项目提供了一个TestEngine用于在平台上运行基于 Jupiter 的测试。
JUnit Vintage 提供了一个TestEngine用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试。它要求 JUnit 4.12 或更高版本出现在类路径或模块路径中。
类似的框架还有 TestNG。
Mockito
借助 Mock(模拟) 框架进行单元测试一直被认为是一种有用的实践,尤其是Mockito框架近年来主导了这个市场。
spring-boot-starter-test
自带 Mockito
PowerMock
PowerMock 由两个扩展 API 组成。
一个用于EasyMock,一个用于Mockito。要使用 PowerMock,您需要依赖这些 API 之一以及测试框架。
目前 PowerMock 支持 JUnit 和 TestNG。
它提供了以一种简单的方式使用 Java 反射 API 的功能,以克服 Mockito 的问题,例如缺乏模拟 final, static or private methods 的能力。
Testcontainers 数据库层
Dao 层单元测试
单元测试中使用数据库,可以考虑两种方案:
- 搭建一个长期使用的测试数据库,作为单元测试,测试开始前或完成后清空无关数据,即可保证测试的可重复性。缺点是多个人同时运行单元测试时,可能会失败。
- 使用内存数据库(如 H2)。优点是无需清空无关数据,缺点是要将数据库初始化过程(如建表语句)纳入单元测试中。如果初始化很复杂,也会影响单元测试的效率。
- Testcontainers 数据库层
- 使用 Spring 的事务控制回滚事务。
|
|
H2
H2 是一个使用 Java 实现的内存内存数据库,支持标准的SQL语法,支持大部分的 MySQL 语法和函数,很适合依赖关系型数据库(比如MySQL, SQL Server, Oracle等)的单元测试。
|
|
Redis
如果依赖了中间件或数据库,做单元测试时,有两种思路:
embedded service (基于内存) TestsContainers(基于容器)
Spring Boot Test
|
|
|
|
Feign
对 Feign 接口进行单元测试时,需要 import 两个 configuration,因此可以抽一个基类出来,如下所示:
|
|
然后继承这个基类,并 enable 要测试的 client 即可。
|
|
maven-surefire-plugin
|
|
http://maven.apache.org/plugins/maven-surefire-plugin/
可能是由于历史的原因,Maven 2/3中用于执行测试的插件不是maven-test-plugin,而是maven-surefire-plugin。其实大部分时间内,只要你的测试类遵循通用的命令约定(以Test结尾、以TestCase结尾、或者以Test开头),就几乎不用知晓该插件的存在。然而在当你想要跳过测试、排除某些测试类、或者使用一些TestNG特性的时候,了解maven-surefire-plugin的一些配置选项就很有用了。例如 mvn test -Dtest=FooTest 这样一条命令的效果是仅运行FooTest测试类,这是通过控制maven-surefire-plugin的test参数实现的。
http://maven.apache.org/surefire/maven-failsafe-plugin/
pre-integration-test
for setting up the integration test environment.integration-test
for running the integration tests.post-integration-test
for tearing down the integration test environment.verify
for checking the results of the integration tests.
The Failsafe Plugin has only two goals:
- failsafe:integration-test runs the integration tests of an application.
- failsafe:verify verifies that the integration tests of an application passed.