Cucumber Ruby 在 JRuby 上的限制:原生语言步骤定义的替代写法与源码级解析
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 方法"的机制:
- lib/cucumber/glue/registry_and_more.rb 在类加载时遍历
Gherkin::DIALECTS的全部方言,收集每个方言的given/when/then/and/but关键字,通过Glue::Dsl.alias_adverb逐一将其别名为register_rb_step_definition; - lib/cucumber/gherkin/i18n.rb 提供
code_keyword_for,负责把 Gherkin 关键字清洗成可用作方法名的形式——去掉空白、引号、逗号和感叹号,code_keywords_for还会剔除*(星号通配步骤); - 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 上使用多语言场景的实践建议
综合官方限制说明与仓库实现,可以总结出以下可操作的结论:
- 判断平台:可通过
Cucumber::JRUBY(定义于 lib/cucumber/platform.rb)在运行时检测是否处于 JRuby 环境,便于编写条件逻辑或动态加载英语化步骤。 - feature 文件放心用原生语言:只要首行声明
# language: xx,Функционал/Сценарій/Функционал等方言关键字均可正常解析执行。 - 步骤定义规则:俄、乌、乌兹别克三种语言在 JRuby 上必须使用
Given('本地文本 {int}') do |english_var|的英语化写法;文本与参数占位符保持不变,{int}、{string}、{float}、{word}等 Cucumber 表达式照常可用(详见三种语言的步骤定义示例)。 - 块变量名保持英语:这是官方明确标注的硬性要求,与 DSL 关键字同等重要,不能只改关键字不改变量名。
- 留意个别平台的测试差异:在 JRuby 上运行完整测试套件时,部分与
failure procs输出、query 行为相关的用例会被官方跳过(如 spec/cucumber/formatter/json_spec.rb、spec/cucumber/query_spec.rb),排查问题时需知悉这是已知的平台差异而非代码缺陷。
总而言之,在 JRuby 上使用 cucumber 的要点是:平台支持是完整的,feature 的多语言能力是完整的,唯一需要适配的是这三种西里尔系语言的步骤定义 DSL 层。将步骤定义改写为"英语关键字 + 原生文本 + 英语变量"即可无缝迁移,且不影响既有 feature 文件与步骤文本的匹配关系。