Cucumber Ruby 在 JRuby 上的限制:原生语言步骤定义的替代写法与源码级解析

原创2026-10-14 12:19:241,831 阅读
文章标签:测试质量保障

Cucumber Ruby 在 JRuby 上的限制:原生语言步骤定义的替代写法与源码级解析

cucumber 可以在 JRuby(官方文档标注已在 10.0 上验证)上正常运行,但存在一个值得注意的平台差异:俄语(ru)、乌克兰语(uk)、乌兹别克语(uz)这三种语言的步骤定义无法用原生语言关键字编写。本文以 docs/jruby-limitations.md 为主线,结合仓库源码与 examples/i18n 下的真实示例,说明该限制的具体表现、底层机制,以及"feature 文件保留原生语言、步骤定义英语化"的正确迁移方案,帮助你在 JRuby 上继续使用多语言场景。

JRuby 平台支持概况

根据官方限制说明,cucumber 可运行在 JRuby 上(已用 JRuby 10.0 验证),绝大多数功能不受影响。从源码看,仓库对 JRuby 是显式识别并做了专门适配的:

  • 平台常量定义在 lib/cucumber/platform.rb:JRUBY = defined?(JRUBY_VERSION),运行时只要存在 JRUBY_VERSION 常量即判定为 JRuby 环境;
  • 异常堆栈过滤在 lib/cucumber/formatter/backtrace_filter.rb 中为 JRuby 额外追加了 org/jruby/ 路径,用于在打印失败信息时剔除 JRuby 运行时自身的内部调用栈,让报错更贴近用户代码;
  • 交互式 ask 输入在 lib/cucumber/runtime/user_interface.rb 中做了分支:MRI 使用 Timeout.timeout 包裹 $stdin.gets(第 65-71 行),JRuby 则改用 java.lang.Thread 启动读取线程并用 Thread#join(timeout_seconds * 1000) 实现超时(第 73-81 行),以规避 JRuby 上 Ruby 层 Timeout 行为的不确定性。

此外,仓库自身的测试也承认 JRuby 存在个别行为差异。例如 spec/cucumber/formatter/json_spec.rb 中注释说明"由于 JRuby 输出 failure procs 的方式"而跳过部分用例,spec/cucumber/query_spec.rb 也直接标注"这些测试在 JRuby 上无法通过"。这说明官方对 JRuby 的态度是"可用但非一等公民",个别边缘特性按平台跳过。

核心限制:三种语言无法用原生语言定义步骤

官方文档明确列出,目前有三种语言——俄语(Russian)、乌克兰语(Ukrainian)、乌兹别克语(Uzbek)——的步骤定义不能使用原生语言关键字编写。也就是说,下面这样的代码在 JRuby 上是不成立的:

Допустим('я ввожу число {int}') do |число|
  calc.push(число)
end

这里 Допустим(俄语 Given)会被当作 Ruby 方法名来调用,число 会被当作块参数(局部变量名)。在 JRuby 上,这三类基于西里尔字母/扩展字符的关键字无法作为步骤定义方法名正常注册,因此必须退回英语写法。

底层机制:关键字到 Ruby 方法的动态别名

为什么 MRI 上能写 Допустим(...)?从源码结构看,原生语言步骤定义依赖的是"把 Gherkin dialect 关键字动态别名化为 Ruby 方法"的机制:

  1. lib/cucumber/glue/registry_and_more.rb 在类加载时遍历 Gherkin::DIALECTS 的全部方言,收集每个方言的 given/when/then/and/but 关键字,通过 Glue::Dsl.alias_adverb 逐一将其别名为 register_rb_step_definition;
  2. lib/cucumber/gherkin/i18n.rb 提供 code_keyword_for,负责把 Gherkin 关键字清洗成可用作方法名的形式——去掉空白、引号、逗号和感叹号,code_keywords_for 还会剔除 * (星号通配步骤);
  3. lib/cucumber/glue/dsl.rb 的 alias_adverb 通过 alias_method 把这些关键字挂到 register_rb_step_definition 上,于是 Given、When、Then 与各语言对应关键字在 Ruby 语义上完全等价。

因此,能否用原生语言写步骤定义,本质上取决于该语言的关键字能否安全地成为 Ruby 方法名。在 JRuby 上,俄、乌、乌兹别克三种语言的关键字(如 Допустим、Припустимо、Агар)无法完成这一映射,官方因此在 CI 中对这三种语言统一采用"英语化步骤定义"(anglicised step definitions)的测试方式。

正确写法:关键字与块变量用英语,步骤文本保留原生语言

官方给出的替代写法要点是(原文特意标注):Gherkin 关键字(DSL 术语)和块变量(block variable)都用英语,而引号内的步骤文本保留原生语言:

Given('я ввожу число {int}') do |number|
  calc.push(number)
end

对比两种写法可以看出:

维度 原生写法(JRuby 不支持) 英语化写法(JRuby 支持)
DSL 关键字 Допустим Given
步骤匹配文本 я ввожу число {int} я ввожу число {int}(不变)
块变量名 число number

需要强调的是,引号内的步骤文本与参数占位符(如 {int})完全保留原生语言,因此 feature 文件与步骤定义之间的文本匹配关系不受影响——只有 DSL 方法名和块变量名需要英语化。

feature 文件不受限制:仍可完全使用原生语言

该限制只作用于 Ruby 步骤定义代码,.feature 文件则完全不受影响。你依然可以用纯原生语言编写 feature 文件并在 JRuby 上执行,例如官方文档给出的俄语示例:

# language: ru
Функционал: Сложение чисел
  Чтобы не складывать в уме
  Все, у кого с этим туго
  Хотят автоматическое сложение целых чисел

  Сценарий: Сложение двух целых чисел
    Допустим я ввожу число 50
    И затем ввожу число 70
    Если я нажимаю "+"
    То результатом должно быть число 120

只需在文件首行保留 # language: ru 声明,Cucumber 就会用对应的 Gherkin 方言解析其中的 Функционал、Сценарий、Допустим、И、Если、То 等关键字,再与英语化的步骤定义 Given('я ввожу число {int}') 等进行匹配。从 lib/cucumber/gherkin/steps_parser.rb 可以看到,解析时通过 Gherkin::Dialect.for(@language) 按语言取方言、并透传 default_dialect 选项,语言声明在解析链路中全程生效。

仓库中的真实示例:ru / uk / uz 三语对照

仓库的 examples/i18n 目录为每种语言提供了完整的可运行工程(feature + step_definitions + lib + Rakefile),其中三种受限语言恰好是官方 CI 采用英语化步骤定义的代表:

俄语(ru)——examples/i18n/ru/features/step_definitions/calculator_steps.rb:

Given('я ввожу число {int}') do |number|
  calc.push number
end

对应 feature 文件 examples/i18n/ru/features/addition.feature 以 # language: ru 开头,步骤文本为 Допустим я ввожу число 50。

乌克兰语(uk)——examples/i18n/uk/features/step_definitions/calculator_steps.rb:

Given('потім/я ввожу число {int}') do |number|
  calc.push number
end

对应 examples/i18n/uk/features/addition.feature 使用 Припустимо、І、Якщо、То 等乌克兰语关键字。

乌兹别克语(uz)——examples/i18n/uz/features/step_definitions/calculator_steps.rb:

Given('{int} сонини киритсам') do |number|
  calc.push number
end

对应 examples/i18n/uz/features/addition.feature 使用 Агар、Ва、Унда 等关键字。从这些示例可以清晰看出统一的工程实践:所有步骤定义一律使用 Given/When/Then,步骤匹配文本与占位符保留本地语言,块参数使用英语标识符。

作为对照,其他语言(如法语、德语、日语等)在 examples/i18n 中仍可直接用原生关键字定义步骤,因为它们的方言关键字在 JRuby 上可以完成方法名映射——这正是本限制只波及三种语言的原因。

在 JRuby 上使用多语言场景的实践建议

综合官方限制说明与仓库实现,可以总结出以下可操作的结论:

  1. 判断平台:可通过 Cucumber::JRUBY(定义于 lib/cucumber/platform.rb)在运行时检测是否处于 JRuby 环境,便于编写条件逻辑或动态加载英语化步骤。
  2. feature 文件放心用原生语言:只要首行声明 # language: xx,Функционал/Сценарій/Функционал 等方言关键字均可正常解析执行。
  3. 步骤定义规则:俄、乌、乌兹别克三种语言在 JRuby 上必须使用 Given('本地文本 {int}') do |english_var| 的英语化写法;文本与参数占位符保持不变,{int}、{string}、{float}、{word} 等 Cucumber 表达式照常可用(详见三种语言的步骤定义示例)。
  4. 块变量名保持英语:这是官方明确标注的硬性要求,与 DSL 关键字同等重要,不能只改关键字不改变量名。
  5. 留意个别平台的测试差异:在 JRuby 上运行完整测试套件时,部分与 failure procs 输出、query 行为相关的用例会被官方跳过(如 spec/cucumber/formatter/json_spec.rb、spec/cucumber/query_spec.rb),排查问题时需知悉这是已知的平台差异而非代码缺陷。

总而言之,在 JRuby 上使用 cucumber 的要点是:平台支持是完整的,feature 的多语言能力是完整的,唯一需要适配的是这三种西里尔系语言的步骤定义 DSL 层。将步骤定义改写为"英语关键字 + 原生文本 + 英语变量"即可无缝迁移,且不影响既有 feature 文件与步骤文本的匹配关系。

登录后查看全文
cucumber-ruby