自分用メモ。
Feature specを書くときに使うTurnipはとてもシンプルで、
Cucumberのようにフォーマッターとか特に提供されていないんだけど、これだけシンプルなら自分で簡単に作れそう。
RSpecの枠組みに乗せるところ
指定されたfeatureファイルをもとに describe
を作っている。
def run(feature_file)
feature = Turnip::Builder.build(feature_file)
return nil if feature.nil?
instance_eval <<-EOS, feature_file, feature.line
context = ::RSpec.describe feature.name, feature.metadata_hash
run_scenario_group(context, feature, feature_file)
EOS
end
run_scenario_group のなかで、個々のBackground/Scenarioループを回す。
- BackgroundはRSpecのbefore
- Scenarioはdescribe、step群の実行はit
- Scenarioのメタデータがdescribeの第2引数(メタデータハッシュ)に渡されているのがポイント
context.before do
background_steps.each do |step|
run_step(filename, step)
end
end
group.scenarios.each do |scenario|
step_names = (background_steps + scenario.steps).map(&:to_s)
description = step_names.join(' -> ')
context.describe scenario.name, scenario.metadata_hash do
instance_eval <<-EOS, filename, scenario.line
it description do
scenario.steps.each do |step|
run_step(filename, step)
end
end
EOS
end
end
ステップの実行
動的に定義されたメソッドを呼び出しているだけ。
matches = methods.map do |method|
next unless method.to_s.start_with?("match: ")
send(method.to_s, description)
end.compact
if matches.length == 0
raise Turnip::Pending, description
end
if matches.length > 1
msg = ['Ambiguous step definitions'].concat(matches.map(&:trace)).join("\r\n")
raise Turnip::Ambiguous, msg
end
send(matches.first.method_name, *(matches.first.params + extra_args))
stepの定義
「動的に定義」はどこでやっているかというと、
def step(method_name=nil, expression, &block)
if method_name and block
raise ArgumentError, "can't specify both method name and a block for a step"
end
step = Turnip::StepDefinition.new(expression, method_name, caller.first, &block)
send(:define_method, "match: #{expression}") { |description| step.match(description) }
send(:define_method, expression, &block) if block
end
個々のstepに対して、
-
match: #{expression}
っていう名前のメソッド- descriptionが正規表現マッチするかどうかだけを調べる
-
#{expression}
っていう名前のメソッド- stepの実行
の2つを生やしている。
expressionとは...? と若干気になったので、 Turnip::StepDefinition
にログを仕込んで見てみると、
step 'open Top page' do
visit 'http://awesome.example.com/'
end
というstepで試すと、 method_name = nil
expression = 'open Top page'
だった。
引数を2つ指定できりょうになっているのは、method as a step 用なのだろう。
そもそもstep一覧はどこで読み込んでいる?
https://github.com/jnicklas/turnip#where-to-place-steps
READMEに書いてあるように、spec_helperで明示的にrequireしたstepしか読まれないのである。
Turnipは上記のstep定義のための #step
メソッドを提供しているだけ。
まとめ
FeatureをRSpec変換する部分と、Step定義部分を読んでみた。
Feature一覧は
feature_files = Dir.glob('features/**/*.feature')
feature_files.each do |feature_file|
feature = Turnip::Builder.build(feature_file)
feature.children.each do |background_or_scenario|
background_or_scenario.steps.each do |step|
end
end
end
みたいにループを回せる。
Step一覧は
# READMEの手順どおりにロード
Dir.glob('steps/**/*_steps.rb') { |f| load f ; true }
# 動的に定義されたメソッドをすべてインクルード
include Turnip::Steps
# https://github.com/jnicklas/turnip/blob/master/lib/turnip/execute.rb を参考にいろいろ...
match_methods = methods.select{|m| m.to_s.start_with?("match:")}
...
のように、定義されたマッチメソッドを拾えば、特定のstepにマッチするstep定義を取れる。
応用すれば、CIでお行儀の悪いFeature Specを取り締まるとかできそうかも・・・!
追記: すっごい雑だけどstepチェッカー作ってみた
CIで ruby check.rb
をすれば features/ steps/ を走査して
- 未定義のstep
- 複数の定義に該当してしまい、どれを使えばいいのか決められないstep
- 未使用のstep definition
を抽出できます。
Calling steps from other steps
は「このプロジェクトでは使わないでください」としてありますw (実装がむずいので諦めたw)