RSpecコードリーディング（第2部：Turnip）


[RSpecコードリーディング（第1部：RSpec）](http://qiita.com/orangain/items/32ce0ac113d607fab003)の続きです。

以下の疑問を解消するために[Turnip](https://github.com/jnicklas/turnip)のコードを読んでいきます。

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



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

- [rspec/rspec-core at v2.14.7](https://github.com/rspec/rspec-core/tree/v2.14.7)
- [jnicklas/turnip at 1.2.1](https://github.com/jnicklas/turnip/tree/1.2.1)

## 第2部：Turnip

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

### 1. .rspecファイル

[TurnipのREADME](https://github.com/jnicklas/turnip)には、`.rspec`ファイルに`-r turnip/rspec`を追加すると書いてあります。このファイルは何でしょうか。

[RSpecのドキュメント](http://rubydoc.info/gems/rspec-core/file/README.md#store_command_line_options__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.
```

このコマンドライン引数で渡されたディレクトリは、[前回](http://qiita.com/orangain/items/32ce0ac113d607fab003#3-9)も見た`Configuration#setup_load_path_and_require`の引数`paths`に渡されます。そして以下のコードにあるように、specをロードする前のタイミングで`require`されます。

```ruby: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::Configuration`に`Turnip::RSpec::Loader`を`include`しています。

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

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

```ruby: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#load`は`rspec/core/configuration.rb`の896行目で使われている`Kernel#load`を覆い隠して、`.feature`ファイルの場合に別の処理を行います。

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

```ruby: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
```

```ruby: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のスタイルでテストしているようです。

```ruby: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`をパースしています。

```ruby: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](https://github.com/jnicklas/turnip)には、`spec_helper.rb`に以下のように書くよう指示があります。

```ruby: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のコードを読むのは辛いところもありましたが、いろいろトリッキーな書き方を知ることができて面白かったです。