LoginSignup
0
0

More than 3 years have passed since last update.

Turnipのコードリーディング

Last updated at Posted at 2020-01-24

自分用メモ。

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)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0