简化Java单元测试数据

news2024/11/13 20:46:39

用EasyModeling简化Java单元测试

EasyModeling 是我在2021年圣诞假期期间开发的一个 Java 注解处理器,采用 Apache-2.0 开源协议。它可以帮助 Java 单元测试的编写者快速构造用于测试的数据模型实例,简化 Java 项目在单元测试中准备测试数据的工作,在提高编写效率的同时,使单元测试更加整洁易读。经过一年的维护,EasyModeling 已经在几个 Thoughtworks 内部的项目上得到了应用,并迭代发布了几个版本。

单元测试中的数据准备的困难

在企业级应用软件开发项目中编写测试代码时,针对特定的测试场景,我们需要准备相应的测试数据,以验证被测组件在给定输入下的行为。在使用 Java 语言的项目中,这些准备测试数据的代码体现为创建各种“数据模型类”的实例。这里的数据模型类,可以包括聚合模型(Aggregation Model)、数据传递模型(DTO)、值对象(VO)以及存储模型(Persist Model)等等。无论是对服务组件的测试,还是对数据模型本身的测试,我们都无可避免地需要构建这些数据模型类的实例。

在项目的起初阶段,准备数据的工作是简单的,我们只需要调用数据模型类的构造方法,传入适当的参数来创建实例即可。单元测试代码的规模不会太大,也尚且清晰易读。

但是随着产品开发工作的展开,一方面,项目中使用的这些数据模型会变得越来越复杂;另一方面,测试场景也会变得越来越多。经验上,在经过几个版本迭代的企业级应用 Java 代码中,我们通常不难找出一些拥有十几个、甚至几十个成员变量的数据模型类,并且它们之间还存在着复杂的相互持有、嵌套、继承的关系。这些数据模型类往往都是项目中的核心组件,故而也成为单元测试需要重点关注的组件。相应地,在涉及这些数据模型的单元测试中,为准备测试数据而编写的初始化数据模型类的代码量也会越来越大、越来越复杂。

这些冗杂繁复的数据初始化代码会影响单元测试本身的代码质量,造成单元测试编写成本高、易读性差、易维护性低等问题。而单元测试的质量又与生产代码的质量息息相关。例如,单元测试的编写成本过高,会使开发者越来越倾向于仅在已有测试基础上做修改,而不是为每个场景创建单独的测试,造成单个测试的职责过多;甚至使开发者放弃单元测试,降低了团队对产品质量的信心。又比如,单元测试的易读性差,导致单元测试无法承担起“测试即文档(tests as documentation)”的职责。而单元测试的易维护性低,则导致了代码很难被重构,从而单元测试不仅没有为重构提供信心,反而变成重构的桎梏。

具体来说,这些初始化数据的代码会引起三个方面的问题:

  • 对测试场景的描述不清晰
  • 构建测试数据的代码重复
  • 初始化数据模型代码的膨胀

我们可以从下面的例子中略窥端倪。你是否在你的项目中见过这样的单元测试?

这是一段典型的使用JUnit测试框架的单元测试代码。在这段单元测试代码中,被测对象是 leaveCalculator 组件的 annualLeave 方法。我们首先创建一位员工,如(a)处;然后将创建好的员工对象传入 annualLeave 方法,为其计算出应得的年假数额,如(2)处;最后断言他应该享有20天年假,如(3)处。为了简化讨论,我们暂且假设此处 annualLeave 方法的业务规则是:员工应得的年假数额只与这位员工加入公司的时间(date of joining)相关,即在代码中 (1) 处初始化的日期。

我们来详细分析这段测试代码中存在的坏味道、以及其潜在的问题。

对测试场景的描述不清晰

如前文所述,我们假设这段单元测试代码的目的是验证“入职超过5年的员工应该享有20天年假”这个业务规则。那么显然,其中只有 (1), (2), (3) 这三处是与当前测试场景相关的,它们共同构成了对上述业务规则的描述。而在 (1) 处之前传入 Employee 类构造方法的那些参数都是与当前测试场景无关的。遗憾的是,这些与测试场景无关的代码却占据了这个代码片段中的绝大部分代码行。

在实际项目中,我们会见到很多这样的单元测试,它们往往需要用几十行的代码来准备复杂的测试数据,需要初始化数个数据模型类的对象,以支持对被测组件的调用,然而这些代码中真正在描述测试场景的,却只有其中区区几行、甚至一两行。这不仅增加了测试的篇幅,还会导致阅读者无法快速聚焦在有意义的初始化条件上。就像我们在这个例子中看到的,描述测试场景的代码行(1)处混杂在大量初始化测试数据的代码行之中,造成了单元测试对测试场景的描述不聚焦。这使单元测试的阅读者很难从这段测试代码中一目了然地理解测试的意图,更遑论以测试为文档来理解业务规则。而在测试失败时,也无法快速从测试场景的数据构造出发去定位问题。

一些有经验的单元测试编写者已经注意到了这个问题,他们会在关键的测试数据初始化行末添加一些注释以示强调。然而注释本身就预示着代码坏味道,并且在重构中也是非常不安全的,甚至反而误导读者。

构建测试数据的代码重复

如果将目光从单个测试放大到单元测试组(Test Suit),我们会发现在针对同一个被测组件的不同测试场景下,初始化数据模型的代码会大量重复。例如在针对员工年假数额计算(leaveCalculator 组件的 annualLeave 方法)的测试组中,假设按照业务规则,我们需要考虑以下的测试场景:

  1. 入职不足2年的员工,应该享有10天年假;
  2. 当年入职的员工,享有按照入职时间折算的年假数额;
  3. 入职超过2年,而不足5年的员工,应该享有15天年假;
  4. 入职超过5年的员工,应该享有20天年假;
  5. 入职超过7年的员工,应该享有25天年假;
  6. 入职时间在未来(尚未入职)的员工,不应该计算年假数额(抛出异常);

不难想象,我们会分别在这6个测试场景对应的测试方法中重复地编写几乎完全相同的代码来初始化Employee类的对象。

这样的单元测试模式在企业级应用开发的场景中比比皆是。开发者经常很容易在测试第二个场景时,顺手从第一个场景的单元测试中复制初始化数据模型的代码,略作修改来描述第二个测试场景,后面的测试场景也如法炮制。这样显然会造成测试代码中存在大量的模板代码(Boilerplate code),进一步降低了代码的易读性。

通常在开发项目的实践中会引入构建者模式(Builder Pattern)或者 Object Mother 组件来消除这些模板代码。本文非常欣赏这些解决方案,下文会在此基础上做进一步讨论。

初始化数据模型代码膨胀

另外需要注意的是,前文举例的代码中为节省篇幅已经做了很多简化。我们不仅用省略号折叠了(1)处之后可能传入构造方法的更多的初始化参数,还折叠了在(b)处初始化 List<Department> departments 参数时逐个构造 Department 类对象所需要的大量细节,甚至在初始化每个Department类对象时,又另外需要构造更多的相关实例。

当然在实践中,经常使用的策略是将大量无关的属性设置成 null 或者空集合,但是这有时候会在被测组件对数据类有效性检查中被拦截。特别是在某些演进了一段时间的代码库中,我们经常会遇到的困难是,由于在测试中构造数据时采用了过多的 null 和空集合,一个新添加的数据有效性检查步骤或者切面(AOP),会造成几百个单元测试的失败。逐一修复这些失败的单元测试的工作量无疑是巨大的,同时是充满风险的,因为此时对单元测试的修改完全是为了兼容一个新添加的切面,而脱离了单元测试本身的业务上下文。

在这种情况下,开发者会越来越多选择将相似的数据有效性检查步骤散布在具体的业务代码中,而非在构造方法中统一检查、或者通过切面集中实现。可见,单元测试的不良设计,会反过来增加生产代码的维护难度,拖累了生产代码的演进。

EasyModeling提供的能力

造成开发者写出类似单元测试的原因是广泛存在的。例如,Employee 类没有提供更灵活的构造方法,也没有 Builder 模式的构造器。从 Employee 类自身的职责的角度出发,它的确没有理由提供一个仅包含 LocalDate dateOfJoining 作为参数的构造方法。在很多业务场景下,数据模型类也完全有可能就是不允许通过 Builder 模式来构造的。我们当然不能为了编写测试代码的便利,而去修改生产实现代码。又例如,代码中可能存在对 Employee 类的数据合法性校验。这些校验可能是类似切面的形式存在的,导致我们无法方便地在单元测试中忽略它。

在实际项目中,开发者很容易从“消除重复”的角度,抽象出相应的工厂类来提供测试所需要的数据模型实例。Martin Fowler 也在他的博客的短文 Object Mother 中简要讨论了相关的思路。但是在测试中使用工厂组件虽然消除了很多重复代码,却没有提供针对不同的测试场景的灵活定制能力,因此一些项目又会同时采用 Builder 模式来提供定制能力。我自己在多个项目上引入 Object Mother 来提供测试数据实例后发现,这些工厂类本身又具有非常固定的代码模板,于是我开始考虑开发一个工具来自动生成这种工厂类。

受到 Builder 模式和 Object Mother 思想的启发,我开发了 EasyModeling 来尝试简化 Java 单元测试的编写,并提高测试的可读性和易维护性。EasyModeling 是一个 Java 注解处理器库,它主要提供三个方面的功能:

  1. EasyModeling在编译期根据指定的数据模型类的结构,生成对应的数据模型工厂类,以方便单元测试快速生成数据模型类的实例。通过向 EasyModeling 注册一个数据模型类,单元测试的编写者只需要调用 EasyModeling 所提供工厂类的静态方法,就可以立即得到这个数据模型类的实例。
  2. EasyModeling 还可以在单元测试的运行时,自动初始化它所生成的数据模型实例。在生成数据模型实例时,EasyModeling 默认的行为是给数据模型实例的字段填充随机值,让开发者不需要再耗费精力去填充对测试场景无意义的属性。同时,开发者仍然有机会向 EasyModeling 指定每个数据模型类的每个字段所需的初始化方式。
  3. 另外,EasyModeling 还在其生成的工厂类中提供了一个 Builder 模式的构建器。利用这个构建器,开发者可以定制、并仅定制与当前测试场景相关的字段,使单元测试简短、清晰、易读。

在编码层面,EasyModeling 的行为完全发生在测试包中,丝毫不会侵入项目的生产实现代码。同时,EasyModeling 只会照顾开发者向它注册的数据类型类,而不会在代码库中主动搜索。所以即使是维护已久的代码库,从任何时间点引入 EasyModeling 都不会造成额外的负担。

EasyModeling简化后的单元测试

在引入了 EasyModeling 后,本文中第一节中的单元测试例子可以得到显著地简化:

除此之外,如前文提到,开发者需要在测试代码中向 EasyModeling 注册 Employee 类:

首先我们看到,在引入 EasyModeling 后,单元测试的代码在篇幅上得到了非常明显地简化。在单元测试中 (4) 处,EmployeeModeler 类就是由 EasyModeling 在编译期生成的工厂类,通过引用 EmployeeModeler 类中的静态方法 builder(),我们可以得到 Employee 类的Builder 的实例。请注意,此处使用的 Builder 类不是由 Employee 类自己编写的,也不是通过如 Lombok 这样的工具来提供的,而是由 EasyModeling 在其生成的工厂类 EmployeeModeler 来提供的。这样的好处是,为了测试而准备的 Builder 完全没有侵入生产代码。

其次,在 (4) 处生成的 Builder 类的实例中,EasyModeling 已经为我们尽可能多地填充了所有的成员变量。因此,我们接下来只需要聚焦在当前测试场景所关心的成员变量上。例如在 (5) 处,我们将 dateOfJoining 字段的内容设置为指定的日期。在可读性方面,由于避免了冗长的初始化参数,所以使开发者在阅读单元测试时,能够快速理解测试场景,进而也比较容易修改或维护单元测试。

第三,EasyModeling 在填充数据模型实例的属性时,不仅能够填充一些 Java 应用中常用的数据类型,包括基本类型、数组、集合、时间日期等等,还能够进一步填充当前数据模型所引用的其他数据模型。例如 Employee 类中引用的 List<Department> departments 列表字段。

最后,为了让 EasyModeling 帮我们生成 Employee 类的工厂类,如以上代码中 (6) 处,开发者只需要在任意的一个类上通过 @Model 注解声明即可。EasyModeling在编译期为所有被 @Model 注解声明的数据模型类生成对应的工厂(Modeler)类。

除此之外,EasyModeling 还提供了其他一些好用的特性,限于篇幅,具体的用法请参考文档。

EasyModeling的不足和未来

但是由于我的业余精力和能力都非常有限,EasyModeling 目前还处于它成长的初期,存在几点显然的不足。

第一,没有维护良好的使用文档。目前我只维护了一份项目 Readme 文件,作为简要的使用文档,导致一些略高级的使用方法和一些从新版本开始支持的功能并没有体现在文档中。

第二,没有维护文档注释。遵循代码整洁的原则,在长期从事的企业应用开发中,我几乎不会写任何形式的注释。所以我也没有意识到,在维护一个更偏底层的开源工具库时,充分的文档注释是非常必要的。一方面,文档注释便于开发者用户查看阅读,也便于有兴趣的贡献者参与开发。另一方面,由于这种较为基层的工具中无可避免地要使用一些魔法,如果没有良好的注释,随着时间推移,可能连我自己也会忘记其中的细节。

由于 EasyModeling 是一个关注单元测试的工具,而不会入侵任何生产代码,因此,在 Java 项目中引入 EasyModeling 几乎不会对项目的可靠性、安全性造成任何风险。所以如果你对这个工具感兴趣,认为它有可能帮助你提高编写测试的效率,请不妨引入到你的项目中尝试使用。

未来,由于我自己在项目上会持续使用 EasyModeling 来构建测试数据,所以我基本可以保证持续维护这个工具。在近期,我将聚焦在完善使用文档,以及修复从用户反馈的一些缺陷。在EasyModeling 的功能特性方面,虽然我手上目前依然积压着一些我自己想要实现的功能,但是我更想从用户的反馈中收集更多有趣的好主意,再来推进下一阶段的功能演进。


文/Tthoughtworks张哲
原文链接:用EasyModeling简化Java单元测试-Thoughtworks洞见

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/802080.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

半路杀出个“程咬金”,谁在吹响智能化供应链重构号角

汽车智能化的竞争&#xff0c;不再是平行模式&#xff08;车企与车企、Tier1与Tier1&#xff09;&#xff0c;也不再是一边倒的车企自研模式&#xff0c;更不是纯粹的B2C模式。 随着昨天大众集团对外官宣与小鹏、上汽的深度合作启动&#xff0c;围绕电动化、智能化的竞争无疑进…

PHP注册、登陆、6套主页-带Thinkphp目录解析-【白嫖项目】

强撸项目系列总目录在000集 PHP要怎么学–【思维导图知识范围】 文章目录 本系列校训本项目使用技术 上效果图主页注册&#xff0c;登陆 phpStudy 设置导数据库项目目录如图&#xff1a;代码部分&#xff1a;控制器前台的首页 其它配套页面展示直接给第二套方案的页面吧第三套…

Talk | 南洋理工大学博士后研究员李祥泰:基于Transformer的视觉分割模型总结、回顾与展望

​ 本期为TechBeat人工智能社区第517期线上Talk&#xff01; 北京时间7月27日(周四)20:00&#xff0c;南洋理工大学博士后研究员—李祥泰的Talk已经准时在TechBeat人工智能社区开播了&#xff01; 他与大家分享的主题是: “基于Transformer的视觉分割模型总结、回顾与展望”&am…

无涯教程-jQuery - animate()方法函数

animate()方法执行一组CSS属性的自定义动画。 animate( params, [duration, easing, callback] ) - 语法 selector.animate( params, [duration, easing, callback] ); 这是此方法使用的所有参数的说明 params - 动画将朝其移动的CSS属性图。duration - 这是可选…

idea常用技巧/idea常见问题

idea常见问题 idea全局搜索默认只显示100条解决方案 如上图&#xff0c;每次搜索时只显示100条&#xff0c;没法展示全。因版本的不同&#xff0c;配置也有些差异&#xff0c;以下也是经过各种搜索整理出了两个方案来解决这个问题。 方案一&#xff1a; 快捷键Ctrl shift a…

windows环境启动redis-server.exe出现闪退问题解决方案(亲测有效)

现象 windows环境下&#xff0c;启动redis-server.exe&#xff0c;出现闪退现象 解决方案 在你的redis解压目录下&#xff0c;新建一个start.bat文件 在start.bat文件里面写上这一句话&#xff1a; redis-server.exe redis.windows.conf然后保存&#xff0c;后面启动redis…

网络安全/信息安全—学习笔记

一、网络安全是什么 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防两面…

【暑期每日一练】 day7

目录 选择题 &#xff08;1&#xff09; 解析&#xff1a; &#xff08;2&#xff09; 解析&#xff1a; &#xff08;3&#xff09; 解析&#xff1a; &#xff08;4&#xff09; 解析&#xff1a; &#xff08;5&#xff09; 解析&#xff1a; 编程题 题一…

​LeetCode解法汇总2500. 删除每行中的最大值

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣 描述&#xff1a; 给你一个 m x n 大小的矩阵 grid &#xff0c;由若干正整数组成。 执行下述操作…

方差分析学习

有时候能看的下去&#xff0c;有时候看书看不下去。 为了加深理解和记忆&#xff0c;先把基础的内容记忆一下。 组内方差&#xff08;误差平方和&#xff09;和组间方差&#xff08;效应平方和&#xff09;的定义&#xff1a; 注意&#xff1a;图中红线部分是一个整体。 误差…

基于java SpringBoot和HTML的博客系统

随着网络技术渗透到社会生活的各个方面&#xff0c;传统的交流方式也面临着变化。互联网是一个非常重要的方向。基于Web技术的网络考试系统可以在全球范围内使用互联网&#xff0c;可以在本地或异地进行通信&#xff0c;大大提高了通信和交换的灵活性。在当今高速发展的互联网时…

Quartz实战:基于Quartz实现定时任务的动态调度,实现定时任务的增删改查

文章目录 一、Quartz基础二、使用Quartz实现定时任务的动态调度1、使用Quartz-jobStore 持久化2、前端页面实现效果图3、自定义job表4、增删改查Controller5、Quartz工具类6、测试任务类7、springboot启动初始化定时任务8、自定义JobFactory&#xff0c;使Task注册为Bean9、省略…

PoseiSwap 即将开启质押,利好刺激下 POSE通证短时涨超 30%

随着Nautilus Chain主网的上线&#xff0c;预示着Web3世界迎来全新的模块化、Layer3时代&#xff0c;为Web3世界与Web2世界的深入融合构建基础。而PoseiSwap作为Nautilus Chain上的首个DEX&#xff0c;也成为了加密行业首个以模块化为基础构建的DEX。 基于Nautilus Chain&#…

免费商城搭建之java商城 开源java电子商务Spring Cloud+Spring Boot+mybatis+MQ+VR全景+b2b2c 手机商城免费搭建

1. 涉及平台 平台管理、商家端&#xff08;PC端、手机端&#xff09;、买家平台&#xff08;H5/公众号、小程序、APP端&#xff08;IOS/Android&#xff09;、微服务平台&#xff08;业务服务&#xff09; 2. 核心架构 Spring Cloud、Spring Boot、Mybatis、Redis 3. 前端框架…

【lesson6】gcc和动静态库介绍

文章目录 gcc介绍gcc使用gcc -Egcc -Sgcc -cgcc *.ogcc *.c一步到位翻译 动静态库介绍动态链接和静态链接动态链接静态链接动静态链接总结 gcc介绍 gcc是一款翻译器&#xff0c;专门用来翻译C语言文本的翻译器。 程序的翻译过程&#xff1a; 是C文本---->计算机二进制可执行…

Qt5.14.2+VS2019配置MSVC2017

问题&#xff1a; The compiler " Microsoft Visual C Compiler 16 . 11 . 32106 . 194 ( amd64 x86 )( x86-windows-msvc2019-pe-32bit ) cannot produce code for the Qt version " Qt5.14.2 MSVC2017 64bit " ( x86-windows-msvc2017-pe-64bit 编译器“…

Docker安装部署ShardingProxy详细教程

&#x1f680; ShardingSphere &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&…

求整数中的最大值

才用打擂台的形式&#xff0c;先放一个数max在擂台上&#xff0c;然后每个数都上去和他比较&#xff0c;如果上去的那个数比max大&#xff0c;那么就把max替换成那个数站在擂台上&#xff0c;依次比较&#xff0c;直到所有数都比较完后&#xff0c;站在擂台上的那个max就是最大…

码元、波特率、比特率、频带利用率及数字通信系统的可靠性指标

文章目录 前言一、码元二、码元传输速率 R B R_B RB​&#xff08;传码率、波特率&#xff09;三、信息传输速率 R b R_b Rb​&#xff08;传信率&#xff0c;比特率&#xff09;四、 R B R_B RB​ 和 R b R_b Rb​的关系——H&#xff08;信源的熵&#xff09; 为纽带五、频带…

四步从菜鸟到高手,Python编程真的很简单(送书第一期:文末送书2本)

&#x1f341;博主简介 &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01; &#x1f40b; 希望大家多多支持&#xff0c;我…