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