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
されます。
# @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で説明します。
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
を実行しています。
# @private
def load_spec_files
files_to_run.uniq.each {|f| load File.expand_path(f) } # 896行目
raise_if_rspec_1_is_loaded
end
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のスタイルでテストしているようです。
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
をパースしています。
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
に以下のように書くよう指示があります。
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のコードを読むのは辛いところもありましたが、いろいろトリッキーな書き方を知ることができて面白かったです。