利用 JavaFX 26 Headless 模式在 GitHub Actions 中实现自动化测试
目录
当我发布 Lottie4J 1.1.0 时,我在发布说明和这篇博客文章中提到了一件略显尴尬的事情:虽然有一个新的单元测试用于对比 JavaFX 播放器的输出与 JavaScript 参考播放器的输出,但它“无法在 CI 上运行,因为它需要显示输出”。这是一个待办事项(TODO),一个已知的限制。这是那种你写下时寄希望于未来的自己能解决的笔记。
JavaFX 26 于 2026 年 3 月 17 日发布,并包含了一个新的无头(headless)平台,这使我能够在没有显示器的情况下在 GitHub Actions 上运行该测试。
测试及其重要性
Lottie4J 的核心挑战在于正确性。Lottie 格式复杂,包含大量嵌套数据,我的 JavaFX 渲染器必须产生与 JavaScript 播放器显示效果相匹配的输出。“像素级完美”的目标过于宏大,但“是否足够接近”是一个合理的衡量标准。
在开发过程中,我使用了 Lottie4J 项目中的一个独立应用程序:LottieFileDebugViewer。这是一个 JavaFX 应用程序,它加载 Lottie 文件,并分别通过 JavaFX 播放器和官方 Lottie 播放器的 WebView 进行渲染。这使得比较结果并深入数据结构和不同图层以调试差异变得非常容易。
[LOADING...]
基于这个调试查看器,我创建了一个包含两个步骤的单元测试方法:
-
一个 WebViewScreenshotGenerator,我会在开发机器上运行一次。它使用 LottieFiles JavaScript 播放器在 JavaFX WebView 中加载每个动画,并捕获特定帧的截图。这些作为参考图像并被提交到仓库中。
-
单元测试 CompareFxViewWithWebViewTest 随后使用 Lottie4J JavaFX 播放器渲染相同的动画,在相同的帧处截取屏幕,并将像素数据与参考图像进行比较。
参考图像生成一次并提交。测试仅检查 JavaFX 的输出是否与其保持一致。如果渲染器出现问题,测试就会捕获到。
这一切在本地运行良好。问题在于 GitHub Actions。CI 运行环境没有显示器,也没有图形堆栈。所以我用以下代码在 CI 中禁用了此测试:
JavaFX 26 的变化
JavaFX 26 添加了一个直接内置于 javafx.graphics 模块中的无头平台原型(Headless Platform Prototype)。无需额外依赖,无需原生库,无需 Monocle 设置。只需传递一个 JVM 标志:
就是这样。JavaFX 启动后,你将获得一个功能完备的工具包,可以创建场景、渲染节点、截取快照并运行动画,所有这些都无需连接显示器。Gluon 团队在 JavaFX 26 中完成了这项繁重的工作,这使得 JavaFX 组件的 CI 测试变得更加实用。该标志的工作方式与正常运行应用程序相同。不同之处在于没有任何内容会被绘制到屏幕上。出于测试目的,这正是你所需要的。它还为服务器端渲染打开了大门,例如,可以在没有显示器的情况下生成 UI 组件的快照。
关键点:JavaFX 26 需要 Java 24
Lottie4J 的目标是 Java 21 和 JavaFX 21。这是大多数项目仍在运行的 LTS 版本。由于该版本已被广泛采用,我不希望仅仅因为我想要更高级的测试基础设施,就强迫库用户跳跃到更新的版本。因此,主项目目前仍保持在 21 版本。
但 JavaFX 26 需要 Java 24 或更高版本才能运行。他们在该版本中将编译的字节码级别提升到了 --release 24,因此如果你尝试将其与旧版本的 JDK 一起使用,会立即收到错误。这意味着测试基础设施必须使用与主构建不同的 Java 和 JavaFX 版本。我最终采用的解决方案是在根 pom.xml 中使用 Maven Profile,它会覆盖两个版本属性并配置 surefire 插件:
当此 Profile 激活时,Maven 会将 java.version 提升至 25,将 javafx.version 提升至 26,因此依赖解析会在测试类路径中获取 JavaFX 26,而主源代码仍然编译为 Java 21 目标。然后,surefire 插件将两个 JVM 参数传递给测试 JVM:
-Dglass.platform=headless告诉 JavaFX 使用新的无头 glass 后端,而不是尝试连接到显示器。--enable-native-access=javafx.graphics是必需的,因为无头平台使用了 Java 模块系统否则会阻止的原生代码路径。
--add-opens 行赋予了测试运行器访问其加载和比较渲染输出所需的 fxfileviewer 模块内部结构的权限。
fxfileviewer/pom.xml 和 fxplayer/pom.xml 通过标准的 Maven 继承获取被覆盖的 javafx.version 属性,因此当 Profile 激活时,这些模块会自动在测试类路径中获得 JavaFX 26。
GitHub Actions 端的配置
Maven 工作流使用 Java 25 JDK 设置环境,以便 JavaFX 26 运行时可以加载,并使用该 Profile 调用 Maven:
构建的其余部分仍然针对 Java 21 目标进行编译,因此库本身不受影响。该 Profile 仅在测试运行时生效。工作流不需要任何显示设置,不需要 Xvfb,也不需要调整 DISPLAY 环境变量。无头标志处理了所有这些!
实际测试内容
单元测试比较了由 JavaFX 播放器渲染的 Lottie 动画与来自 JavaScript 播放器的预生成参考图像的截图。它加载一组已知的动画文件,渲染每个文件的特定帧,使用 WritableImage 和 SnapshotParameters 获取快照,然后进行具有可配置容差的像素级比较。
其结果是一个在每次推送时都会运行的回归测试。如果有人以明显破坏动画的方式更改了渲染逻辑,CI 将会捕获它。这比听起来更有用,因为 Lottie 渲染涉及大量的分层转换、缓动函数和形状操作,很容易引入细微的错误。
我推荐这种模式吗?
是的,但有一些注意事项。
版本管理确实需要额外的工作。如果你想在保持库使用旧版 Java 的同时使用 JavaFX 26 无头模式进行测试,你需要小心地将测试 JVM 配置与主构建分离开来。Maven 使这变得可行,但并不算优雅。
参考图像方法也需要纪律。参考图像需要一致地生成(最好是在可复现的环境中),并且你需要考虑什么样的容差对于你的比较是有意义的。如果太严格,测试会变得不稳定;如果太宽松,你就会错过真正的回归错误。
但回报是实实在在的。我曾经标记为“无法在 CI 上运行”的测试现在可以在 CI 上运行了。没有虚拟帧缓冲区,没有 Docker 技巧,无需人工干预。JavaFX 启动、渲染动画,比较过程干净利落地完成。
对于任何在 JavaFX 中进行视觉渲染的库来说,这都是以前真正缺失的测试基础设施。做得好,OpenJFX 的贡献者们!
相关链接: