RSpec
Turnip
e2e

Turnipで他のシナリオを実行するステップ

状況

TurnipでGivenScenarioしたい!

Turnip向けに書いたGherkinシナリオが、他のシナリオに書いたステップで実現される状況を前提としている。
このため、前に書いたシナリオAを今書こうとしているシナリオBにおいて「前提」ステップとして再利用したい。

こんな風に書けたらいいな

機能: Hoge
  背景:
    前提 Fugaする

  シナリオ: Aを登録
    もし Piyoする
        ならば PiyoPiyo

  シナリオ: Bを登録
    前提 「Aを登録」しておく
    もし Hogeraする
        ならば HogeraHogeHoge

Cucumberの各ホスト言語における実装は知らんけど、Turnipの場合割とシンプルに内部でRSpecのdescribe ... context ... it構造にマップされているため頑張ればなんとかなる。

このfeatureに対応して内部で生成されているのはだいたいこういうコードだ。

feature = Turnip::Builder.build(feature_file)

describe "機能 Hoge" do
  background_steps = feature.backgrounds.map(&:steps).flatten
  before {
    background_steps.each do |step|
      run_step(feature_file, step)
    end
  }

  feature.scenarios.each do |scenario|
    describe scenario.name do
      # "Bを登録 前提 「Aを登録」しておく -> もし Hogeraする -> ならば HogeraHogeHoge"みたいなやつ
      description = ()

      it description do
        scenario.steps.each do |step|
          run_step(filename, step)
        end
      end
    end
  end
end

我々がstepの定義を書くときはこのrun_stepから呼び出された中にいる。従って、現在のitブロックへのアクセスだけできればscenarioにもfeatureにもアクセスできる。
これを利用すると次のように書ける。

step '「:name」しておく' do |name|
  context = RSpec.current_example.metadata[:block].binding
  current_feature = context.local_variable_get(:feature)
  filename = context.local_variable_get(:filename)

  scenario = current_feature.scenarios.find {|scenario| scenario.name == name }
  scenario.steps.each do |step|
    run_step(filename, step)
  end
end

今後の課題

残念ながら、このままでは他のfeatureに属するシナリオにアクセスすることはできない。
ローカル変数featureはそのfeatureのためのdescribeを構築しているブロック内にしか存在しないので、そのままでは他のfeatureを現在のit内から参照する術はないのである。

ただし、RSpecがdescribeをネストしたRSpec::Core::ExampleGroupのサブクラスにマッピングする内部APIはもう何年も安定していて、そこそこ安心して利用できそうだ。したがって、ObjectSpace.each_objectでそれっぽいExampleGroupインスタンスを浚って目標のfeatureに相当するものを特定すれば、なんとかする余地はありそうだ。

警告!

Cucumberにおける公式の見解は「そういう機能はサポートしない」らしい。代わりに、Cucumberのホスト言語で高度なヘルパーメソッドを書いてそれをステップ定義で利用したり、前提状況を再現するWorldを作ったりするのが推奨されている。

本稿はそれでもなんとかしたいという話であるが、以下に見るように(少なくともcucumberでは)あまり歓迎されていない手法である。

http://blog.josephwilk.net/ruby/cucumber-waves-goodbye-to-givenscenario.html

I initially thought this was a great feature...
However... in the longer term it produces scenarios that are hard to maintain and understand

https://github.com/cucumber/cucumber-jvm/issues/502#issuecomment-16068777

this is a common request that we are not going to implement

https://github.com/cucumber/cucumber-js/issues/237#issuecomment-53555187

We implemented this feature (we call it "nested steps") in the Ruby Cucumber, and it's been hard to maintain, and led to some very complex dependencies in people's test code.