Java单元测试实践-00.目录(9万多字文档+700多测试示例)
https://blog.csdn.net/a82514921/article/details/107969340
1. 前言
以下内容为本人以开发人员的视角,在平时进行单元测试过程中的总结。主要内容为通用的,不限制具体业务场景的单元测试实践总结。由于能力有限,经验不足,难免会有差错存在,希望与大家一起探讨。
2. 示例工程
以下所述示例工程为UnitTest,地址为( https://github.com/Adrninistrator/UnitTest) 或( https://gitee.com/adrninistrator/UnitTest ),包含测试类400多个,测试方法700多个。
示例工程UnitTest中还提供了以下单元测试可以使用的公共方法:
方法 | 说明 |
---|---|
TestCommonUtil.getTestNum() | 获取指定测试类的@Test方法数量 |
TestCommonUtil.checkObjectValue() | 检查对象字段值是否等于预期值的简化方法 |
TestReplaceUtil.replaceMockMember() | 将对象中的指定类型成员变量替换为Mock对象并返回的简化操作 |
TestReplaceUtil.replaceSpyMember() | 将对象中的指定类型成员变量替换为Spy对象并返回的简化操作 |
TestCommonExecutionListener.afterTestMethod() | 当前测试类所有测试方法执行完毕时,关闭数据源 |
TransactionalTestErrorSkipExecutionListener.afterTestMethod() | 使单元测试使用事务,未出现异常时回滚数据库操作,出现异常时不回滚数据操作 |
使用JUnit5的示例工程为UnitTest_JUnit5,地址为( https://github.com/Adrninistrator/UnitTest_JUnit5 )或( https://gitee.com/adrninistrator/UnitTest_JUnit5 )。
根据数据库表生成JPA Entity的Java组件增强版,地址为 https://github.com/Adrninistrator/jpa-entity-generator-enhance ,在开源项目的基础上进行了优化。
3. 依赖环境版本
以下为使用的依赖环境的相关版本:
类型 | 名称 | 版本号 |
---|---|---|
操作系统 | Windows | Windows 7 |
Java | JDK | 1.8.0_144 |
IDE | IntelliJ IDEA | 2019.2.4 (Community Edition) |
IDE | Eclipse | 2019-06 (4.12.0) |
构建工具 | Gradle | 6.2 |
数据库 | MariaDB | 10.0.36 |
数据库 | H2 | 1.4.200 |
Java组件 | JUnit | 4.13 |
Java组件 | Spring | 4.3.26 |
Java组件 | Mockito | 3.4.6 |
Java组件 | Powermock | 2.0.7 |
4. 单元测试可以做什么
单元测试可以完成以下目标。
4.1. 提高代码质量
单元测试可以对被测试的方法逐个验证是否符合预期,从而提高整个功能模块或系统的代码质量。
缺陷发现得越早,修复起来越简单。单元测试可以在开发阶段尽量提前发现缺陷,降低缺陷修复成本。
在编写单元测试用例的过程中,开发人员需要重新查看被测试的代码,检查代码结构是否合理。如果单元测试用例不容易编写,说明代码结构可能需要调整。编写单元测试可以促使开发人员优化代码结构。
单元测试代码虽然不在生产环境运行,但可以提升生产代码的质量。
4.2. 提升开发效率
单元测试使得开发人员可以方便地在开发环境执行编写的代码,降低执行被测试代码的操作复杂度与时间消耗,提高自测时效性,对于明显的异常和错误能够快速地发现并及时修复。
相比其他类型的测试,单元测试有以下优势:
- 可以免去每次进行测试前提交代码的步骤,在本地进行编译、打包、发布等操作更便捷;
- 执行测试操作及测试结果检查均通过程序执行,不需要人工操作;
- 单元测试用例中保存了测试数据及预期结果,便于获取测试数据,及理解被测试代码的功能。
在单元测试过程中,通过Mock等方式,可以尽量降低对外部系统的依赖,只需要专注于自身系统的功能。
单元测试可以通过程序检查测试结果是否与预期一致,减少了对于人工检查的依赖,提高了执行结果验证的效率与准确率。
有的代码执行对时间存在依赖,如定时任务、批处理等,通过单元测试可以随时对此类代码进行测试,不受时间的限制。
编写单元测试用例也需要时间,从整体阶段来看,单元测试还是能够提升效率的。
4.3. 降低异常情况的测试复杂度
单元测试可以对被测试代码进行Mock,通过相对简单的操作就可以模拟各种异常情况,不需要依赖其他关联系统进行调整。相比其他类型的测试,通过单元测试对异常情况的测试更便捷,可以覆盖更多代码分支。
4.4. 起到部分回归测试的作用
对现有代码的变更是不可避免的,若相关功能的代码拥有对应的单元测试用例,可以通过单元测试快速验证代码变更是否对现有功能产生了影响,对存量功能修改的影响更可控,质量更有保障,起到部分回归测试的作用。只需要编写一次单元测试代码,就可以重复执行,对代码进行验证,获取单元测试来带的便利( 代码有变动时,对应的单元测试代码也要修改 )。
单元测试也可以与CI/CD相关的工具结合,自动化运行,减少人工干预。
4.5. 以直观的方式展示重要功能
单元测试代码中,能够以直观的方式(对方法的Mock及对执行结果的检查等),展示被测试代码的重要功能,例如远程调用、数据库操作,及执行结果、执行完毕后的数据库数据等。
4.6. 处理开发相关的安全问题
应用程序可能会由于开发不当产生一些安全问题,如跨站、SQL注入、任意文件下载、危险文件上传、越权、其他的逻辑漏洞等。
通常需要安全人员通过代码扫描或渗透测试等方式发现以上问题,向开发人员反馈问题及解决方法,开发人员再根据安全人员的建议对问题进行修复。
开发人员通常并不了解安全人员使用的渗透测试工具与方法,如果需要直接使用安全人员的渗透测试工具与方法,学习成本会比较高,因此导致开发人员很难参与此类安全问题的检测。由于开发人员缺乏参与安全问题检测的有效实践方法,使得开发人员难以对以上安全问题得到进一步的了解。因此形成了一个死结,因为难以实践,所以缺乏了解;因为缺乏了解,因此容易出现安全问题。
单元测试可以作为解决以上问题的一个突破口。当开发人员了解了安全问题的原理后,可以通过单元测试验证上述问题,在经过实际的安全问题验证后,对于安全问题的原因、检测及修复方法,都会有更深刻的理解,安全问题的出现机率应会逐渐下降,形成良性的循环。
对于开发人员,掌握单元测试比掌握安全人员使用的渗透测试工具和方法更容易,通过单元测试验证开发相关的安全问题,是开发人员更熟悉,更容易接受的方法,实现难度与学习成本更低。
开发人员通过单元测试验证安全问题,是通过白盒的方式进行测试,开发人员对于程序功能、代码逻辑和可能出现的问题更清楚,存在一定的优势。
5. 哪些代码需要进行单元测试
最理想的情况下,对所有的代码都进行单元测试,代码质量是比较有保证的。但在实际的开发过程中,时间可能会比较紧张,对于工具类或公共代码以及重要的业务代码,最好能进行单元测试,保证重要的方法是经过测试符合预期的。
在最初的阶段,即使在单元测试用例中只是简单地调用了被测试的方法,还不满足AIR原则( 如没有通过Mock解决环境依赖问题,需要手工解决;没有通过程序检查执行结果,还需要人工检查等 ),相比没有单元测试的代码也是提高。先解决有无问题,再进行优化。在使用单元测试的过程中,会逐渐熟悉所使用的单元测试框架,编写单元测试代码会逐步变得更加高效,并得到单元测试带来的便利,从而获得正反馈。
6. 单元测试需要关注的场景
6.1. 方法入口参数检查
对于方法入口的请求参数,需要进行单元测试,测试正常及异常情况下的处理是否符合预期。如果对于请求参数通过注解或配置的方式进行检查,则可以转变为对应的公共检测方法进行单元测试。
6.2. 业务功能的主要流程
对于程序提供的业务功能的主要流程,需要进行单元测试。根据程序设计的结果分析并选择需要测试的场景,是一种比较有效的方式,所选择的场景是有依据的,不是主观臆断得来的。
可以通过思维导图、表格等形式记录需要进行单元测试的各个场景,方便检查是否有出现重复或遗漏,至少保证能够覆盖大部分重要的场景。
需要进行单元测试的场景包含包括正常场景与异常场景,最好能够覆盖全部的场景。需要检查被测试代码在不同场景下的执行结果是否符合预期,主要包括如下内容:
- 方法返回值或对于入参的修改是否符合预期;
- 数据库记录中的重要属性值是否符合预期;
- 生成文件的情况,及文件内容是否符合预期;
- 发起的远程调用,及请求数据内容是否符合预期;
- 作为服务提供方时,向上游系统返回数据的重要属性是否符合预期。
7. 单元测试代码编写建议
7.1. AIR原则
参考(阿里巴巴)Java开发手册,“【强制】 好的单元测试必须遵守 AIR 原则”。其中AIR分别代表A:Automatic,自动化;I:Independent,独立性;R:Repeatable,可重复。
7.1.1. A-自动化
自动化是指单元测试是自动执行,非交互式的,执行过程中不需要人工介入。
自动化可以大致分为执行过程自动化、执行结果检查自动化与单元测试执行动作触发自动化,其中执行过程自动化与执行结果检查自动化可以通过单元测试用例实现,单元测试执行动作触发自动化依赖其他条件,在后续内容中讨论。
7.1.1.1. 执行过程自动化
使单元测试执行过程自动化,不需要进行交互,不依赖人工处理,摆脱对执行环境的依赖,可能需要使用Mock等方式。
7.1.1.2. 执行结果检查自动化
在单元测试中可以使用断言等方式,自动对执行结果进行检查,不需要人工干预,结果更准确,效率更高。
执行结果检查包括但不限于检查方法返回值、检查方法执行完毕后数据库记录、检查远程调用方法是否执行及请求数据是否符合预期。
7.1.2. I-独立性
单元测试用例需要满足独立性,每个用例都需要是简单、明确的。一个单元测试用例,通常是针对一个方法,或者一个功能的一组方法进行测试。
各单元测试用例之间不能相互调用,在执行顺序上也不能有依赖。若单元测试执行时存在依赖数据,需要在各单元测试用例中自己解决,如设置Mock,或通过代码插入所需数据等。
7.1.3. R-可重复
单元测试用例需要是可重复的,执行结果应与执行次数无关。各个测试用例之间需要是相互独立的,测试用例也需要独立于运行环境,可能需要使用Mock。
7.2. BCDE原则
参考(阿里巴巴)Java开发手册,“【推荐】编写单元测试代码遵守BCDE原则,以保证被测试模块的交付质量。”。其中BCDE分别代表B:Border,边界值测试;C:Correct,正确的输入;D:Design,与设计文档相结合;E:Error,强制错误信息输入。
7.3. 其他建议
当发现代码缺陷时,需要检查是否有对应单元测试用例,若无则需要补充测试用例;若有则需要检查用例,是否覆盖不全,并进行优化。
通过对某个方法或功能的正常、异常情况的各种场景编写单元测试用例,判断实际执行结果与单元测试中预期的结果是否一致,可以验证单元测试代码本身是否正确。
8. 单元测试与Mock
以下不明确区分Mock与Stub等概念,以下所述的Mock均是指在不修改代码的情况下,对被测试代码的功能进行动态的修改。不同的开发语言通常都存在对应的Mock框架。
- Mock代码对正常功能的影响
Java的Mock代码通常在test模块中编写,在发布时不会影响main模块,避免出现Mock代码影响正式环境的问题。
- Mock范围选择
在进行Mock时,需要克制,只Mock必须的最小范围,避免范围过大时导致部分内容未被测试。
- 了解Mock框架的目的
需要对使用的Mock框架加深了解,避免使用时出现错误,便于选择最合适的使用方式,提高单元测试开发效率,便于后续维护。
8.1. 使用Mock的目的
使用Mock,是为了能够低成本任意改变被测试代码行为,构造所需的测试场景,且能够对方法调用的请求参数等进行检查。
以上的低成本,是指能够高效快速地编写所需的Mock代码,且便于后续维护。
为了达到以上目的,需要尽量了解mock框架,在需要Mock时选择合适的实现方式。
8.2. 需要Mock的代码
从场景看,主要包括以下几类代码需要进行Mock:
-
被测试代码所依赖的环境配置相关代码,用于屏蔽环境依赖,如从本地文件或数据库等获取配置参数;
-
影响被测试执行分支相关代码,主要用于测试不同场景下代码执行结果是否符合预期;
-
通过远程服务调用访问其他系统的相关代码,用于测试依赖的其他系统返回不同数据时执行是否正确。
从操作类型看,操作以下类型的代码均可能需要进行Mock:
进程内方法调用、操作系统配置、文件、网络访问、数据库相关操作等。
8.3. 不需要Mock的代码
Mock应最小化,被测试代码的重要逻辑处理相关代码不需要Mock,若Mock后会导致相关功能无法被验证。例如对重要交易数据执行的数据库插入、修改操作,不需要Mock,可在操作执行完毕后从数据库查询对应数据,判断是否符合预期。
8.4. 数据依赖与Mock的使用
在进行单元测试时,存在依赖数据库记录的情况,比如需要从数据库查询用户的开户信息,交易流水信息等。
当只需要从数据库查询,不需要执行修改或删除操作时,可以将对应的读取操作进行Mock。相比插入数据的方式,不需要进行数据清理;
当需要从数据库查询,并进行修改或删除操作时,可以提前通过代码插入对应的数据,在测试执行完毕后查询最新的数据,检查程序执行是否符合预期。
9. 单元测试执行步骤
单元测试执行步骤(Java)如下图所示:
9.1. 执行测试代码
执行测试代码时,可以使用JUnit、spring-test、Gradle,以及IDE工具。
9.2. 数据初始化
数据初始化包括但不限于以下内容:
- 准备执行被测试方法的请求数据;
- 准备被测试代码需要使用(更新、删除等,读取可以使用Mock)的数据库记录;
- 准备Mock指定的返回数据。
对于数据初始化,个人倾向使用Java代码实现,与使用配置文件、数据库等方式相比,使用Java代码实现并不复杂,配合DTO的setter方法生成插件使用,代码编写效率并不低,且使用Java代码实现数据初始化还有以下优势:
- 使用Java代码更加通用,不依赖特定组件或处理逻辑,不限制数据格式;
- 当DTO中的变量发生变化时,Java代码编译时会报错,能够明确影响范围,避免出现测试代码与原始代码使用变量不一致的问题;
- 使用Java代码,可以使用常量定义,以及代码复用,减少重复代码;
- 使用Java代码时,能够方便地按照规则生成所需变量值,如流水号、随机数、当前时间等。
9.3. 代码Mock
需要进行代码Mock的范围包括但不限于以下内容:
- 进程内方法调用
- 系统变量读取
- 文件读写
- 远程服务调用
- 数据库操作
代码Mock可以使用Mockito、PowerMock,能够满足单元测试对代码进行Mock的要求。
对代码Mock的请求参数及返回数据管理,也倾向使用Java代码实现,原因同上。
9.4. 摆脱数据库环境依赖
在某些执行单元测试的环境(例如CI/CD服务器),可能无法访问数据库服务器(MySQL等),可以使用本地的文件或内存形式数据库替代数据库服务器,例如H2数据库。
9.5. 检查执行结果
检查执行结果是否符合预期,包括但不限于以下内容:
- 检查方法调用返回值
- 检查方法调用参数
- 检查方法调用次数
- 检查数据库记录
对预期的执行结果管理,也倾向使用Java代码实现,原因同上。
9.6. 生成测试结果报告
使用Gradle执行单元测试完毕后,会生成测试结果报告,可以查看单元测试执行的各个类的执行结果,执行耗时,报错信息等。
9.7. 生成代码覆盖率报告
使用Gradle执行单元测试,可以使用JaCoCo生成代码覆盖率报告。
9.8. 清理测试数据
在进行单元测试时,可以使用自动回滚的方式使单元测试对数据库的修改回滚,对于执行失败的测试方法,建议不回滚,便于通过数据库中保留的记录分析出现问题的原因。在后续内容“数据库操作自动回滚处理”中有详细说明。
若不使用数据库操作自动回滚的方式,可参考以下处理。
单元测试生成的数据,需要有明显的标志与非单元测试生成的数据进行区分,便于清理。
通常情况下,单元测试在数据库中生成的记录内容及数量,不会对其他测试造成影响,可以不定期进行手工或自动化清理。
当使用本地的文件或内存形式的数据库进行单元测试时,不需要考虑清理测试数据的问题,可以直接删除数据库文件。
10. 其他内容
10.1. 单元测试的阶段
根据经验,单元测试可以大致分为以下几个阶段:
10.2. 单元测试能否替代其他类型的测试
单元测试只是对于其他类型的测试的补充,并不能替代。
10.3. 单元测试维护
当功能代码发生变化时,可能需要修改对应的单元测试代码。
为了降低单元测试代码维护成本,以及提高单元测试代码编写效率,可以像编写功能代码一样编写单元测试代码,合理设计单元测试代码的结构,合理使用封装、继承、多态等面向对象编程特性,使用公共方法,避免重复代码。将Mock条件设置收敛到公共方法或测试类的抽象基类中,便于后期持续维护。熟练掌握Mock框架,使用最便捷有效的方式设置Mock条件。
因功能代码发生变化而修改对应的单元测试代码是常见的,与功能代码本身的调整类似。
10.4. 检查单元测试效果
假如在实施了单元测试后发现对于提升开发质量或效率没有帮助,只是增加了开发时间,需要检查出现了什么问题。
10.5. 单元测试时间占比
参考《Test Early and Often》( https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/ee330950(v=vs.110) ),“Writing unit tests should take about 40% of team members’ time.”,编写单元测试可能占团队成员大约40%的时间。
10.6. 单元测试与集成测试
以下涉及的部分测试方法或内容属于集成测试的范围,未进行严格区分。
11. 参考资料
内容 | 链接 |
---|---|
阿里巴巴Java开发规范 | https://102.alibaba.com/downloadFile.do?file=1561031481870/Java-huashanxinban.pdf |
测试金字塔实战 | https://insights.thoughtworks.cn/practical-test-pyramid/ |
单元测试之道Java版——使用JUnit | |
Unit Testing Guidelines | https://petroware.no/unittesting.html |
Test Early and Often | https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/ee330950(v=vs.110) |