问题恰好在于编译器外部环境的不可用性。如果没有它的环境,测试正文处理器将是一个失败的起因。
因为未知的起因,探讨正文处理器的主题仿佛在开发人员中引起了一些原始的恐怖。人们偏向于将正文解决与只有最纯熟的公开巫师能力执行的边界巫术和法术分割起来。不肯定要那样。正文解决不用是藏在床底下的大型吓人怪兽。
图片取自 https://sourcesofinsight.com/monsters-under-the-bed/
毫无疑问,正文解决_的确_存在问题,然而解决这些问题的办法也存在。特地突出的一个问题是对正文处理器进行单元测试的艰难。一组 JUnit 5 扩大 Elementary 解决了一个问题。
此正文解决 Thingamajig 是什么?
对于未启动的用户,正文处理器相似于编译器插件。像它的名字一样,它能够由编译器调用以_解决_正文,即 @Nullable
在编译期间。所述过程涵盖了极其宽泛和含糊的范畴。从简略的值验证到功能完善的可插拔类型零碎(如 checker-framework),包罗万象。一个简略的 @Builder
正文生成器,用于通过像 Dagger 这样的代码生成来进行全面的依赖注入。
在 Java 9 之后,它位于 [java.compiler](https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/module-summary.html)
模块外部。正文处理器中蕴含[Element](https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/lang/model/element/package-summary.html)
s 和[TypeMirror](https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/lang/model/type/package-summary.html)
s 的寓意域,Java 语言的形象语法树(AST)示意模式以及 Javaland 中的反射框架的对应模式。Element
s 示意语法结构,例如办法,数组等,而TypeMirror
s 示意类型,例如援用类型(类)和基元,然而咱们切题了。
为什么这么难?
那么,什么使测试正文解决如此艰难呢?咱们认为,无关正文解决环境的所有内容。咱们并不是在说环境是某种邪恶的怪诞事物,实际上它的设计出奇地令人诧异。问题恰好在于编译器外部环境的不可用性。如果没有它的环境,测试正文处理器将是一个失败的起因。
一款好酒的游戏在须要正文解决环境的正文处理器中为每个办法调用拍摄镜头。
简直所有内容都须要如上所述的正文解决环境。
在这个路口,咱们有四种解决方案来克服这种泡菜:
- 不要为单元测试而懊恼。
- 期待某事,任何事件都会产生。
- 模仿 / 从新实现正文解决环境。
- 将正文解决环境从编译器中走进去。
长话短说,咱们最终成为走私者。
走私者的发现
拖网时,咱们发现了 Google 的编译测试项目,这是一个埋藏在 GitHub 我的项目范畴内的暗藏宝石。通过一些奇妙的技巧,该我的项目设法为单元测试提供了一个正文解决环境,只管有些乏味和无限。摸索该我的项目时,很显著这不是咱们所心愿的灵丹妙药。该我的项目受到一些咱们无法忍受的限度:
- 仅反对 JUnit4。正文解决环境仅可通过 JUnit 规定应用,而 JUnit 5 不再反对该性能。咱们应用 JUnit 5 的工夫最长,并且不打算在不久的未来降级。
- 用于正文解决环境的实用程序受到限制。它_能够工作_,但更符合人体工程学。
- 在测试中无奈遍历一个
Element
或TypeMirror
多个编译文件。这对于将编译后的文件用作测试用例至关重要。 - 正文解决环境的范畴限度。正文解决环境仅限于测试方法的范畴。这很不不便,因为无奈在多个测试之间共享测试状态的初始化。此外,该设计使其本身具备意外的行为。
这并不是说我的项目很_蹩脚_,只是咱们的指标有所不同。实际上,Elementary 的某些局部是基于编译测试的。顾名思义,编译测试专一于测试代码的编译,而不是正文解决。那不是咱们的指标。咱们的指标是简化注解处理器的单元测试。因而,衰弱的剂量后,_“__握住我的啤酒”_,并 没有创造这里综合征,根本我的项目的构想。
小学,敬爱的沃森
以编译测试为根底,咱们着手实现将 Basic 变为事实。从一干二净开始,咱们就有了做出决定的自在,否则这些决定会激发愤恨的暴民与干草叉和火把:
- 仅反对 Java 11 及更高版本。Java 9 中的模块系统对
jdk.compiler
模块和ClassLoader
s 进行了一些重大更改。咱们不想解决。 - 仅反对 JUnit5。咱们不心愿反对咱们不应用的等效 JUnit 4。
咱们应用 Chimera 代码生成工具的教训通知咱们,正文处理器的测试属于经典的黑盒和白盒测试类别。对于小型和 / 或简略的批注处理器,针对示例 Java 源文件在编译器中调用批注处理器更为无效。随着正文处理器的复杂性和大小的减少,针对示例文件运行正文处理器的收益将逐步缩小。隔离和测试各个逻辑组件将不再那么乏味。两种不同的类别具备两组齐全不同的要求。
盒乏味的货色
黑盒测试正文处理器_可能_很乏味。它_并不_必须设置,拆除和配置有数。JavacExtension
至多没有相应地。对于每个测试,JavacExtension
应用给定的正文处理器编译一组测试用例。而后,将编译后果集中到测试方法中以进行后续申明。所有配置均通过正文解决,而无需其余设置或装配。
“他们说眼见为实,所以让咱们持续看上来吧。”
咱们虚构的正文处理器非常简单。它所做的只是查看带有正文的元素是否 @Case
也是字符串字段。如果元素不是字符串或变量,则会显示一条谬误音讯。既然_这么_简略,只需对咱们的正文处理器进行黑盒测试就足够了。
测试咱们虚构的正文处理器也不太艰难。咱们要做的就是在测试类上增加一些正文,创立一些测试用例,查看编译后果,而后瞧!功败垂成!
让咱们合成一下代码片段。
- 通过应用正文测试类
@Options
,咱们能够指定在编译测试用例时应用的编译器标记。在此摘要中,-Werror
批示将所有正告视为谬误。 - 要指定编译器要调用的正文处理器,咱们能够应用正文测试类
@Processors
。正确猜想此片段中的哪个正文处理器没有任何处分。 - 通过用
@Classpath
或正文测试类,能够蕴含测试用例以进行编译@Inline
。能够应用 classpath 蕴含 Java 源文件,@Classpath
而外部字符串@Inline
能够转换为嵌入式源文件进行编译。在此片段中,两个[ValidCase](https://github.com/Pante/elementary/blob/master/elementary/src/test/resources/ValidCase.java)
和[InvalidCase](https://github.com/Pante/elementary/blob/master/elementary/src/test/resources/InvalidCase.java)
都包含在内以进行编译。 - 正文的范畴与指标的范畴相干。如果对测试类进行了正文,则该正文将利用于该类中的所有测试方法。同样,测试方法的正文将仅利用于该办法。
Results
示意编译后果。咱们能够指定Results
作为测试方法的参数来获取编译后果。在这个片段中,process_string_field(...)
将取得的后果ValidCase.java
,同时process_int_field(...)
将收到两个后果ValidCase.java
和InvalidCase.java
。
潘多拉魔盒
这是事件变得真正乏味的中央。白盒测试并不像调用正文处理器那样简略,因为测试试图证实的可能性是有限的。在黑盒测试中,咱们只须要证实已知正文处理器针对固定数量文件的编译后果合乎特定条件即可。相同,在白盒测试中,咱们不晓得为什么,什么以及如何测试组件。咱们能做的最好的事件就是使注解解决环境能够在测试类外部拜访。
“容许类范畴的正文解决环境并不难,编译测试曾经做到了。”
最后咱们也有同样的感觉,男孩是咱们错了。只管编译测试的确提供了正文解决环境,但它仅限于测试方法的范畴。无奈在办法之外拜访所述环境意味着反复且简短的初始化代码,这很麻烦。可悲的是,咱们不能仅仅调整编译测试的技巧,因为它发现与咱们的指标不兼容。
编译测试背地的机密实际上很简略。每个测试方法都由 JUnit 规定拦挡,并包装在正文处理器中,该处理器在处理过程中会调用该办法。该测试随后在 JUnit 规定调用的编译器外部执行。可怜的是,在这种技术中,正文解决环境仅在测试方法时可用。因为 JUnit 生命周期的限度,无奈调整技术来拦挡测试实例的创立并将测试实例注入到正文处理器中。
起初在绘图板上破费了大量工夫,咱们胜利创立了ToolsExtension
。此扩大利用了以下事实:测试实例仅须要拜访正文解决环境。测试不须要在正文处理器中执行。一旦确定了这一点,咱们的技巧就是在创立每个测试实例之前,在守护程序线程上运行带有阻塞正文处理器的编译器。通过将编译暂停在处理器外部,能够使环境可被主线程上的测试实例拜访。仅在执行完所有测试之后,编译才会复原。
这是绘制成果不佳的 MS Paint 图,阐明了整个过程。
让咱们假如,因为咱们在空幻的盒子中形容的虚构处理器的范畴和大小一直增长,它被重构为多个组件,其中一个组件查看元素是否像原始正文处理器一样是字符串变量。
应用 ToolsExtension
来测试正文处理器会产生以下代码片段:
让咱们合成一下代码片段:
- 通过用正文类,
@Inline
咱们能够指定一个内联 Java 源文件,其中ToolsExtension
包含要进行编译的文件。 - 能够通过将
Tools
类或依赖项注入到测试类的构造函数或测试方法中来拜访批注解决环境。在这种状况下,咱们TypeMirrors
应用上的静态方法拜访以后值Tools
。 - 在深刻解释都
@Case
和Cases
将在上面的章节中提供。目前,它只是用于在已编译文件中查找元素的机制。
案例案例
随着的实现 ToolsExtension
,咱们胜利地将正文解决环境从编译器中走私了进去。然而,难题中的最初一块依然存在。咱们如何创立这些元素来测试咱们的代码?该jdk.compiler
模块不提供创立元素的办法。尽管 Element
能够模仿,但远非开发人员敌对。初始化不仅简短,蠢笨而且令人费解,而且很难保障模仿元素的行为与其理论对应的行为匹配。咱们也不能寻求 compile = test 作为领导,因为它没有提供相似的信息。
通过很多头痛之后,咱们设法找到了失落的那一块。让咱们让编译器将用习用 Java 编写的测试用例转换成适宜咱们的元素。这样,咱们防止了元素初始化四周的凌乱,并且生成的代码更容易了解。为此,咱们须要某种形式从编译器中获取元素。在进一步欠缺概念之后,咱们最终开发了 Cases
类和相应的 @Case
正文。
返回潘多拉魔盒(Pandora’s Box)的代码段,让咱们对其进行更具体的剖析。
- 通过
@Case
在 Java 源文件中正文一个测试用例,咱们能够从中获取其对应的元素Cases
。A@Case
也能够蕴含标签以简化检索。 - 通过
Cases
,咱们能够通过案例的标签或索引来获取元素。通过依赖注入,咱们能够在此代码片段中取得Cases
viaTools.cases()
或 like 的实例。
想法墓地
如本文结尾所述,咱们摸索了其余一些最终导致失败的路径。咱们认为它们足够乏味,能够在以下各节中进行探讨。因为该解决方案的不切实际和不可承受的折衷,大多数人最终被搁置了。
不测试正文处理器无疑是一个蹩脚的抉择。仅仅因为测试它们很艰难并不能给咱们跳过它的自在。如果咱们抉择走简略路线,问题只会随着工夫的流逝而好转。此外,大多数正文处理器通常执行代码生成和动态类型剖析。两者都很难解决。
那些期待的人来了。然而,为之奋斗的人们会遇到更好的事件。”
如果 JEP 119:javax.lang.model 实现 JDK 8 附带了 Core Reflection 反对,咱们十分狐疑 Elementary 是否会被构思进去。它通过提供规范实现来解决在编译器内部拜访正文解决环境的问题。可悲的是,它被搁置了,将来的致力仿佛停滞了。因为没有任何可期待的内容,因而对单元测试注解处理器进行张望的办法将是不可行的。
比测试正文解决更艰难的问题是尝试模仿 / 从新实现正文解决环境。因为元素代表 Java 语言的 AST,因而咱们须要与语言标准放弃紧密联系,以确保模仿 / 从新实现的元素的行为不会偏离原始元素。诚实说,这使得测试正文处理器看起来像迪士尼的童话,即便是十英尺长的杆子,咱们也不想碰它。的确存在一些现有的从新实现,然而仿佛曾经被抛弃了多年。最初,归结为导致咱们放弃这一路径的弊大于利。
最初的想法
咱们曾经完结了简化正文处理器测试的旅程。回顾过去,这对 Elementary 来说是相对的爆炸。如何采纳该我的项目仍有待察看。然而,如果有的话,我心愿本文激励您开始应用批注处理器。
总而言之,Elementary 引入了:
- 在
JavacExtension
对黑箱测试和测试简略的注解处理器。 - 一个类范畴的正文解决环境,用于应用正文的测试类
ToolsExtension
。 - 从编译器到测试类获取元素的实用程序。
参考:《2020 最新 Java 根底精讲视频教程和学习路线!》