Turnipのステップ実行毎にスクリーンショット(以下SS)とレンダリングされたhtmlを記録するFormatter、CapturefulFormatterを作りました。 本記事では、簡単な使い方の紹介と、どのようにステップを記録しているかについて記していきます。
背景
Ruby on Railsでの受け入れテストと言えばCucumberが著名ですが、ステップ定義にて正規表現を用いる点や、RSpecとの二本立てとなっている点などが課題となっていました。 この2点を解決すべく生まれたのがTurnipです。Turnipの詳しい説明はるびま42号のTurnip解説記事が詳しいので割愛します。
しかし、Turnip では、テストレポートもRSpecのものを使うため、特にステップの失敗時のエラーメッセージがかなりわかりにくくなってしまいました。
下記に例を示します。とある画面にボタンが描画されていなかったため、ステップ実行に失敗したと予測されるテスト結果です。ですが、結局どんな画面だったのかはわかりません。
ステップ失敗時のSSを撮るために生まれたのが TurnipFormatter および Gnawrnip です。 大変便利なgemですが、TurnipFormatterとGnawrnipの組み合わせは、キャプチャのタイミングがページ遷移毎となっており、細かなタイミングでのキャプチャが得られません。 javascriptによる制御が組み込まれたWebアプリケーションでは、もう少し細かなタイミングでのキャプチャが欲しくなりますし、 受け入れテストのレポートにするならば、ステップごとのキャプチャが欲しくなります。
CapturefulFormatter の機能
CapturefulFormatterは、Turnipのステップごとに、ステップ名とスクリーンショット、描画されたhtmlの3つを記録するシンプルなFormatterです。 各ステップのスクリーンショットと描画されたhtmlが保存されるため、いざバグが発生したときなど、調査がよりスムーズに行えるようになるでしょう。
このFormatter使うことで、こんな感じのテストレポートが生成できます。
非常にわかりやすいレポートが生成されるので、エンジニアだけでなく、他部門のメンバーにもテスト結果がスムーズに共有できます。 また、新しく参加するメンバーのためのマニュアルとしても活用できるかと思います。
レポートのテンプレート等のカスタマイズもサポートしていますので、ユーザ独自のきれいなレポートを作成することも可能です。 その他、実装して欲しい機能の要望などがあれば、Githubにて提案いただければ幸いです。
CapturefulFormatter の実現方法
CapturefulFormatterはRSpecのCustomFormatterの一つとして実装しました。 これは、TurnipはRSpecの エクステンション なので、 Turnipの実行記録をとるCapturefulFormatterもRSpecの機構に則るべきだと考えたためです。
実装するためには二つの課題がありました。
まず、RSpecが各Formatterにどのように通知を行っているかを把握する必要があります。ステップ実行前後の通知をFormatterに通知する方法を調べましょう。 次に、Turnipがどのようにfeatureを実行しているか、特にstepの実行前後がどこにあるのかを把握しましょう。そうすることで、先ほど調べたFormatterへの通知を実際に実装できるようになります。
RSpec はどのように Formatter にイベントを通知するのか
RSpec::Core::Formatters
の RDoc にはビルトインの各種 Formatter
の説明と、 CustomFormatter
の作り方について記載されています。
これだけいろいろな通知を受け取れるということは、何か Formatter
へを通知する共通処理があると予測できるでしょう。
このページを良く読むと、 RSpec::Core::Reporter
へリンク が貼られているのがわかります。
このクラスが通知機構の実装そのものです。
しかし、公開されているメソッドのうち、それっぽいメソッドは report
だけであり、引き数もそれっぽくありません。
確認するために、ソースコードを見てみましょう。
report
の実装を読むと、即座に start
を呼び出しているのがわかります。
# https://github.com/rspec/rspec-core/blob/v3.0.4/lib/rspec/core/reporter.rb#L51
def report(expected_example_count)
start(expected_example_count)
begin
yield self
ensure
finish
end
end
start
では notify
を呼び出すだけです。
# https://github.com/rspec/rspec-core/blob/v3.0.4/lib/rspec/core/reporter.rb#L61
def start(expected_example_count, time = RSpec::Core::Time.now)
@start = time
@load_time = (@start - @configuration.start_time).to_f
notify :start, Notifications::StartNotification.new(expected_example_count, @load_time)
end
notify
で具体的に通知を行っているのがわかります。
# https://github.com/rspec/rspec-core/blob/v3.0.4/lib/rspec/core/reporter.rb#L135
def notify(event, notification)
registered_listeners(event).each do |formatter|
formatter.__send__(event, notification)
end
end
周辺のソースを眺めれば、finish
や、 example_group_finished
なども notify
を使って通知をしているのが見て取れます。
今回は、これらに習い、 step_started
を実装し、その内部で noify :step_started
としてやることとしました。
# こんな感じのメソッドを実装してやれば良さそうだ。
def step_started(step)
notify :step_started, Notifications::StepNotificaton.new(step)
end
ところで、このコードをRSpecのコード中に直接書くのは、あまりスマートではありません。 そこで、今回はモンキーパッチを当てることとし、下記の様なコードが実装されています。
# RSpec に当てるモンキーパッチ
module CapturefulFormatter
module RSpec
module Core
module Reporter
# Formatter へ送る構造体。今回は ExampleNotification の実装を参考にした。
StepNotification = Struct.new(:description, :keyword, :extra_args) do
private_class_method :new
# @api
def self.from_step_object(data)
new data.description, data.keyword, data.extra_args
end
end
def step_started(step)
notify :step_started, StepNotification.from_step_object(step)
end
def step_finished(step)
notify :step_finished, StepNotification.from_step_object(step)
end
end
end
end
end
RSpec::Core::Reporter.send(:prepend, CapturefulFormatter::RSpec::Core::Reporter)
これで、 RSpec::Core::Reporter.step_started
が呼び出せるようになり、CustomFormatterで拾えるようになりました!!
Turnip がどのように step を実行するか
TurnipはRSpecの拡張であり、実行コマンドも
rspec spec/acceptance/attack_monster.feature
のように rspec
を用います。
と、いうことは、* RSpec を拡張している場所がソースコード中にある* はずです。
ファイルの一覧を眺めれば、それっぽいファイルがすぐにみつかるでしょう。 turnip/lib/turnip/rspec.rb です。
このファイル中では Turnip::RSpec::Loader
と Turnip::RSpec::Execute
という二つのクラスが用意され、ファイル末尾で Turnip::RSpec::Loader
を RSpec にパッチしています。
パッチを当てやすいのは、Ruby
の特長ですね!!
それぞれの挙動を、まずは Turnip::RSpec::Loader
の方から見ていきましょう。
この Turnip::RSpec::Loader
は load
を定義し、 RSpec::Core::Configuration
にあてています。
このパッチによって、 RSpec::Core::Configuration
中で load
を呼び出すと、上のコードが呼ばれることとなり、指定されたのが .feature
なら Turnip が処理することになります。
# https://github.com/jnicklas/turnip/blob/v1.2.2/lib/turnip/rspec.rb#L12
module Turnip
module RSpec
module Loader
# ここで load メソッドをオーバーライド
def load(*a, &b)
if a.first.end_with?('.feature') # ロードされたのが .feature ファイルなら...
require_if_exists 'turnip_helper'
require_if_exists 'spec_helper'
Turnip::RSpec.run(a.first) # turnip が起動!!
else
super
end
end
private
def require_if_exists(filename)
require filename
rescue LoadError => e
raise unless e.message.include?(filename)
end
end
end
end
::RSpec::Core::Configuration.send(:include, Turnip::RSpec::Loader)
より具体的には、 RSpec::Core::Configuration.load_spec_files
での挙動が、このパッチのおかげで変化します。
これで、 rspec
コマンド実行時に .feature
ファイルが読み込まれた場合、Turnipが実行されるようになります。
# https://github.com/rspec/rspec-core/blob/v3.0.4/lib/rspec/core/configuration.rb#L1057
module RSpec
module Core
class Configuration
def load_spec_files
files_to_run.uniq.each {|f| load File.expand_path(f) } # パッチを当てることで、この load が Turnip::RSpec::Loader.load になる。
@spec_files_loaded = true
end
end
end
end
さて、 Turnip::RSpec::Loader.load
では .feature
ファイルロード時に Turnip::RSpec.run
を実行していたのを覚えているでしょうか。
次は、いよいよ Turnip::RSpec.run
を見ていきましょう。
少し複雑に見えるが、 Turnip::Builder
で Gherkin をパースして、RSpecの desribe
ブロックを作り上げ、それを実行しているだけのコードです。
シナリオごとに describe
ブロックを定義しており、 it(scenario.steps.map(&:description).join(' -> '))
により1つのシナリオからを1つのit句を作っています。
そして、各ステップの実行は Turnip::RSpec::Execute.run_step
で行われています。
# https://github.com/jnicklas/turnip/blob/v1.2.2/lib/turnip/rspec.rb#L63
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)
Turnip::Builder.build(feature_file).features.each do |feature| # Turnip::Builder で Gherkin をパースします。
::RSpec.describe feature.name, feature.metadata_hash do # feature ごとに RSpec の describe を定義し...
before do # before の定義もします!
example = Turnip::RSpec.fetch_current_example(self)
example.metadata[:file_path] = feature_file
feature.backgrounds.map(&:steps).flatten.each do |step| # before で "前提" の各ステップが呼ばれるようにしてますね。
run_step(feature_file, step)
end
end
feature.scenarios.each do |scenario|
instance_eval <<-EOS, feature_file, scenario.line # シナリオごとに describe を作っています! そして、1 つの it を定義しています!
describe scenario.name, scenario.metadata_hash do
it(scenario.steps.map(&:description).join(' -> ')) do # it は 1 つで
scenario.steps.each do |step| # 中では step をぐるぐるまわしていますね。
run_step(feature_file, step) # ここがステップ実行の本体です
end
end
end
EOS
end
end
end
end
end
end
end
さて、ステップ実行の本体が run_step
だとわかりましたので、早速、 RSpec::Core::Reporter
の時と同様に、 run_step
前後に通知を行うモンキーパッチを作成しましょう。
module CapturefulFormatter
module Turnip
module RSpec
module Execute
# 各ステップを実行する。前後に RSpec::Core::Reporter に向けて、実行内容を通知する
def run_step(feature_file, step)
reporter = ::RSpec.configuration.reporter
reporter.step_started(step)
super(feature_file, step)
reporter.step_finished(step)
end
end
end
end
end
::Turnip::RSpec::Execute.send(:prepend, CapturefulFormatter::Turnip::RSpec::Execute)
ここまでの2つのパッチを当てることで、独自Formatterでstep前後の動きを記録できるようになりました!!
CapturefulFormatterでは、単純に Capybara.current_session.save_screenshot
を行い、SSを保存しています。
むすび
ここまで記してきたように、本記事ではステップごとの SS を記録するFomatterの誕生理由と、その実現方法について述べてきました。 実際のCapturefulFormatterでは、ステップごとの情報を記録した後、テスト終了時にerbをもとにレポートを作成する機能なども実装されています。 しかし、コアとなるステップ前後のhook追加については、上記に示した二つのパッチのみで実現できるのが、おわかりいただけたでしょうか。
本記事の方法を使えば、例えば上記パッチのみを spec/support
以下に実装することで、読者自身のCustomFormatterを定義することも簡単です。
せっかく、わかりやすい受け入れテストの記述ができますし、スクリーンショットも簡単に撮ることができるので、読者の中にそのような要望があれば、本記事を役立てていただければ幸いです。