详细介绍:【JUnit实战3_06】第三章:JUnit 的体系结构(上)

《JUnit in Action》全新第3版封面截图
写在前面
继上一章大致了解JUnit 5提供的新功能、新特性后,这一章又从框架层面探讨了JUnit经历的版本演进过程。这也是往往在大部头的技术类资料中才会见到的“养料”。初学者往往对快速入门的小册子心动不已,对 500 页以上的硬骨头退避三舍,甚至不惜反复找很多本小册子来巩固所学。殊不知想要深耕一门技术,除了充分理解其中的技术特性外,更要熟悉这些新功能诞生时的历史背景和使命。任何特性的引入都不是毫无征兆的,而往往是为了解决当时的某个或某类痛点问题而生的。初学者经常对炒得天花乱坠的热门新词和层出不穷的新技术倍感压力,其中部分原因就是只看到了新技术的多和快,却鲜有对背后的强大推力和技术演进的本质规律有一个冷静客观的把握。本章探讨JUnit架构所经历的版本更迭过程,恰巧可以作为了解上述底层逻辑的绝佳案例。
第三章 JUnit 的体系结构(上)
Architecture is the stuff that’s hard to change later. And there should be as little of that stuff as possible.
那些日后难以变更的部分,应当尽可能减少这类内容。就是架构—— Martin Fowler
本章介绍与 JUnit 架构相关的知识点,从 JUnit 4 的架构特点切入,再讨论旧版架构存在的问题以及新版 JUnit 5 重点关注的部分,让读者对 JUnit 的架构特点及演进过程有一个大致的了解。
3.1 软件架构的概念

如图所示,软件架构的概念类似建筑物的体系结构,刻画了架构在整栋建筑物中的基础性地位。软件架构本身通常难以移动和替换,类似建筑物的底座;位于中间的各个部件则代表具体的设计层(design);最顶部则代表软件的惯用模式(idiom)。虽然底座难以整体移动和替换,构建底座的架构元素反倒应该具备一定的灵活性且易于更换。JUnit 4 向 JUnit 5 的架构演进过程完美诠释了这一观点。
3.2 架构演进要素之——轻量化

为了形象说明轻量化在架构演进中的作用,书中讲述了一个电话簿的故事。面对两家供应商生产的同等价位、同等规格的电话簿,公司采购最终的策略是优先考虑更小巧轻便的那家供应商,因为同等情况下更小巧的尺寸意味着更高的信息密度;从使用习惯来考虑,人们也更愿意优先取用小巧便捷的电话簿。JUnit 5 也遵循了轻量化的原则。
3.3 架构演进要素之——模块化

相比之下,模块化的案例则略显生硬:一个运动鞋制造商为了降低生产成本将产地迁到低成本地区,面对当地频繁的新鞋失窃,决策层最终决定将左右脚的鞋子分开生产,从而节省了雇佣保安的额外费用。真正的模块化带来的核心价值在于灵活性、可复用性和效率的提升,而不是解决“防盗”此种风马牛不相及的疑问。
通过要说明模块化带来的好处,能够类比传统咖啡和定制咖啡之间的差异:旧模式下,每一款咖啡新品都像一个大包大揽的函数,里面涵盖了制作新咖啡的所有步骤,存在的疑问主要是效率低下,点单的灵活性较差,库存管理困难,同时存在创新瓶颈。采用定制模式后,将一杯咖啡解构为基础模块 + 组装规则模块,例如确立几个独立的基础模块:
- 基底模块:意式浓缩、茶汤
- 奶制品模块:纯牛奶、燕麦奶、厚椰乳
- 风味模块:香草糖浆、焦糖糖浆、抹茶粉、巧克力酱
- 顶料模块:奶泡、肉桂粉
以及几个组合规则:
- 一个杯型(如中杯)默认包含1 份 基底和 1 份 风味。
- 更换奶制品或额外增加风味/顶料需额外付费。
- 所有模块通过搅拌和融合这个标准动作来连接。
这样一来,生产效率不仅因模块化大幅提升,同时也贴合了用户的多样化选择,库存管理上也可以精细到某个模块的开销,既优化采购环节,又减少了浪费。
最主要的是通过将复杂系统(整个菜单)拆分为可复用、可互换的模块,最终实现了效率、灵活性和可扩展性 的飞跃。
3.4 JUnit 4 的架构特点
探讨旧版 JUnit 4 的架构有助于更好地理解和使用 JUnit 5,同时也为了兼顾当前大量基于 JUnit 4 构建的测试代码。
旧版 JUnit 发布于 2006 年,当时采用了笼统的单体架构,所有功能都集合在一个 jar 文件内,使用时只需在 classpath 下添加该文件即可。
所谓成也萧何败也萧何,当初 JUnit 4 作为亮点的架构设计反而成了进一步演进的缺陷,并最终催生了 JUnit 5 的问世。
主要体现在以下三个方面:
- 模块化设计:单体设计致使可扩展性不足。
runner测试运行器:JUnit 4自带的解决功能扩展问题的方案之一,通过添加@RunWith注解并传入自定义的runner运行器类,间接实现JUnit 4的功能扩展。该自定义类须实现JUnit 4的抽象org.junit.runner.Runner类,并重写继承的getDescription()方法和run()方法。具体处理逻辑主要利用了Java的反射机制来实现。rules测试规则的设计:JUnit 4提供了另一种扩展方案,须实现接口org.junit.rules.TestRule并重写其apply()方法,然后在利用@Rule注解注入目标测试类。底层逻辑依然是Java反射机制。
因为反射机制破坏了 OOP 编程的封装原则,但书中并未展开解释,这里有必要补充说明。运行基于反射机制编写的 JUnit 4 测试代码,使得开发者可以绕开公共接口,直接窥探目标类或方法的私有状态。尤其是代码中直接设置 method.setAccessible(true) 的操作,相当于直接告诉 JVM 无视 private 这些修饰符,可以随便调用设计者限定的内部逻辑。一旦内部实现发生变更,即便公共接口不受影响,测试代码的运行也会立即失败,无形中推高了运维成本,也使得测试代码变得更加脆弱。
此外,测试方法在 JUnit 4 中还必须声明为 public 公有方法,并且方法名还必须满足特定的命名规则(testXYZ)才能被反射机制成功识别,给人一种 为了满足框架的反射调用需求而书写就是测试代码并不是为了表达测试意图而书写、倒更像的本末倒置的感觉。
最后一个致命缺陷才是书中说的,JUnit 4 只考虑了开发者的测试体验,却忽视了流行的代码工具对集成测试框架迫切需求。主流 IDE 工具和代码构建工具想要集成 JUnit 不得不深入框架内部,只能利用反射机制来访问受保护的私有类、方法或成员变量,不得不让这些工具与 JUnit 深度绑定,为后续升级带来巨大阻力,市场亟盼更轻量的基于模块化设计的全新测试框架,于是 JUnit 5 应运而生。
(上篇完)

浙公网安备 33010602011771号