LoginSignup
21
20

More than 5 years have passed since last update.

RSpecコードリーディング(第2部:Turnip)

Last updated at Posted at 2014-02-17

RSpecコードリーディング(第1部:RSpec)の続きです。

以下の疑問を解消するためにTurnipのコードを読んでいきます。

  • .rspecに書く-r turnip/rspecの意味は?
  • なぜspec_helperの中でstepファイルを自分でloadしないといけないの?
  • Turnipを使った時はrequire 'spec_helper'してないのに読み込まれてて怖い><

RSpecのバージョンは2.14.7, Turnipのバージョンは1.2.1です。この記事中の全てのソースコードは以下のWebサイトからの引用ですが、一部説明のために日本語でコメントを追加してあります。

第2部:Turnip

今回はTurnipがRSpecにどのような修正を加えているかを見ていきます。

1. .rspecファイル

TurnipのREADMEには、.rspecファイルに-r turnip/rspecを追加すると書いてあります。このファイルは何でしょうか。

RSpecのドキュメントを読むと、以下のように書いてあります。

You can store command line options in a .rspec file in the project's root directory, and the rspec command will read them as though you typed them on the command line.

つまり、毎回rspec -r turnip/rspecを実行する代わりに、rspecと実行するだけでよくするための設定ファイルということです。

2. rspec -r

次はこの-rオプションを見ていきます。rspec --helpを実行すると、以下の記述が見つかります。

-r, --require PATH               Require a file.

このコマンドライン引数で渡されたディレクトリは、前回も見たConfiguration#setup_load_path_and_requireの引数pathsに渡されます。そして以下のコードにあるように、specをロードする前のタイミングでrequireされます。

rspec-core-2.14.7/lib/rspec/core/configuration.rb
      # @private
      def setup_load_path_and_require(paths)  # 864行目
        directories = ['lib', default_path].select { |p| File.directory? p }
        RSpec::Core::RubyProject.add_to_load_path(*directories)
        paths.each {|path| require path}
        @requires += paths
      end

3. turnip/rspec.rb

ここからTurnipのコードを見ていきます。

ここでは、以下の2つのモジュールが定義されています。

  • Turnip::RSpec::Loader
  • Turnip::RSpec::Execute

下の方を見ると、95行目でRSpec::Core::ConfigurationTurnip::RSpec::Loaderincludeしています。

また、100行目ではconfig.pattern",**/*.feature"を追加しています。デフォルトだと"**/*_spec.rb,**/*.feature"となります。

.featureファイルはGherkin形式ですので、そのままではRubyが読めません。それを解決するのがTurnip::RSpec::Loaderです。詳しくは4で説明します。

turnip-1.2.1/lib/turnip/rspec.rb
require "turnip"
require "rspec"

module Turnip
  module RSpec

    ##
    #
    # This module hooks Turnip into RSpec by duck punching the load Kernel
    # method. If the file is a feature file, we run Turnip instead!
    #
    module Loader
      def load(*a, &b)
        if a.first.end_with?('.feature')
          require_if_exists 'turnip_helper'
          require_if_exists 'spec_helper'

          Turnip::RSpec.run(a.first)
        else
          super
        end
      end

      private

      def require_if_exists(filename)
        require filename
      rescue LoadError => e
        # Don't hide LoadErrors raised in the spec helper.
        raise unless e.message.include?(filename)
      end
    end

    ##
    #
    # This module provides an improved method to run steps inside RSpec, adding
    # proper support for pending steps, as well as nicer backtraces.
    #
    module Execute
      include Turnip::Execute

      def run_step(feature_file, step)
        begin
          step(step)
        rescue Turnip::Pending => e
          # This is kind of a hack, but it will make RSpec throw way nicer exceptions
          example = Turnip::RSpec.fetch_current_example(self)
          example.metadata[:line_number] = step.line
          pending("No such step: '#{e}'")
        rescue StandardError => e
          e.backtrace.push "#{feature_file}:#{step.line}:in `#{step.description}'"
          raise e
        end
      end
    end

    class << self
      def fetch_current_example(context)
        if ::RSpec.respond_to?(:current_example)
          ::RSpec.current_example
        else
          context.example
        end
      end

      def run(feature_file)
        Turnip::Builder.build(feature_file).features.each do |feature|
          describe feature.name, feature.metadata_hash do
            before do
              example = Turnip::RSpec.fetch_current_example(self)
              # This is kind of a hack, but it will make RSpec throw way nicer exceptions
              example.metadata[:file_path] = feature_file

              feature.backgrounds.map(&:steps).flatten.each do |step|
                run_step(feature_file, step)
              end
            end
            feature.scenarios.each do |scenario|
              instance_eval <<-EOS, feature_file, scenario.line
                describe scenario.name, scenario.metadata_hash do it(scenario.steps.map(&:description).join(' -> ')) do
                    scenario.steps.each do |step|
                      run_step(feature_file, step)
                    end
                  end
                end
              EOS
            end
          end
        end
      end
    end
  end
end

::RSpec::Core::Configuration.send(:include, Turnip::RSpec::Loader)  # 95行目

::RSpec.configure do |config|
  config.include Turnip::RSpec::Execute, turnip: true
  config.include Turnip::Steps, turnip: true
  config.pattern << ",**/*.feature"  # 100行目
end

4. Turnip::RSpec::Loader

Turnip::RSpec::Loader#loadrspec/core/configuration.rbの896行目で使われているKernel#loadを覆い隠して、.featureファイルの場合に別の処理を行います。

具体的には'turnip_helper.rb''spec_helper.rb'があればrequireした上で、Turnip::RSpec.runを実行しています。

rspec-core-2.14.7/lib/rspec/core/configuration.rb
      # @private
      def load_spec_files
        files_to_run.uniq.each {|f| load File.expand_path(f) }  # 896行目
        raise_if_rspec_1_is_loaded
      end
turnip-1.2.1/lib/turnip/rspec.rb
    module Loader
      def load(*a, &b)  # 13行目
        if a.first.end_with?('.feature')
          require_if_exists 'turnip_helper'
          require_if_exists 'spec_helper'

          Turnip::RSpec.run(a.first)  # 5へ
        else
          super
        end
      end

      private

      def require_if_exists(filename)
        require filename
      rescue LoadError => e
        # Don't hide LoadErrors raised in the spec helper.
        raise unless e.message.include?(filename)
      end
    end

5. turnip/rspec.rb(再掲)

Turnip::RSpec.runは66行目で定義されています。

Turnip::Builder.buildは後で見ますが、Gherkin形式のfeature_fileをパースしてるっぽいことはわかると思います。

複雑ですが、読み込んだfeatureをFeature Specのスタイルでテストしているようです。

turnip-1.2.1/lib/turnip/rspec.rb

module Turnip
  module RSpec

    # (略)

    class << self
      def fetch_current_example(context)
        if ::RSpec.respond_to?(:current_example)
          ::RSpec.current_example
        else
          context.example
        end
      end

      def run(feature_file)  # 66行目
        Turnip::Builder.build(feature_file).features.each do |feature|  # 6へ
          describe feature.name, feature.metadata_hash do
            before do
              example = Turnip::RSpec.fetch_current_example(self)
              # This is kind of a hack, but it will make RSpec throw way nicer exceptions
              example.metadata[:file_path] = feature_file

              feature.backgrounds.map(&:steps).flatten.each do |step|
                run_step(feature_file, step)
              end
            end
            feature.scenarios.each do |scenario|
              instance_eval <<-EOS, feature_file, scenario.line
                describe scenario.name, scenario.metadata_hash do it(scenario.steps.map(&:description).join(' -> ')) do
                    scenario.steps.each do |step|
                      run_step(feature_file, step)
                    end
                  end
                end
              EOS
            end
          end
        end
      end
    end
  end
end

6. turnip/builder.rb

Gherkin::Parser::Parserを使ってGherkin形式のfeature_fileをパースしています。

turnip-1.2.1/lib/turnip/builder.rb
require "gherkin"

module Turnip
  class Builder

    # (略)

    class << self
      def build(feature_file)
        Turnip::Builder.new.tap do |builder|
          parser = Gherkin::Parser::Parser.new(builder, true)
          parser.parse(File.read(feature_file), feature_file, 0)
        end
      end
    end

    # (略)

  end
end

第2部まとめ

ここまでで、最初の疑問は大体解決しました。

.rspecに書く-r turnip/rspecの意味は?

require 'turnip/rspec'しています。

なぜstepファイルを自分でloadしないといけないの?

TurnipのREADMEには、spec_helper.rbに以下のように書くよう指示があります。

spec/spec_helper.rb
Dir.glob('spec/steps/**/*steps.rb') { |f| load f, true }

これまで見てきたようにspecファイルやfeatureファイルは自動で読み込まれますが、helper関数扱いのstepファイルは自分で読み込む必要があるのです。

Turnipを使った時はrequire 'spec_helper'してないのに読み込まれてて怖い><

考えれば当たり前なんですが、Gherkin形式のfeatureファイルはrequire 'spec_helper'することができないので、Turnipが自動で読み込んでくれます。Turnipだけで必要なコードはturnip_helper.rbに書くこともできます。

RSpecコードリーディングまとめ

RSpecコードリーディングはこれで終わりです。ちゃんとコードを読むことで漠然とした不安を解消できました。

自由度が高すぎてRubyのコードを読むのは辛いところもありましたが、いろいろトリッキーな書き方を知ることができて面白かったです。

21
20
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
21
20