Rubyist Magazine - エンドツーエンドテストの自動化は Cucumber から Turnip へ が出てたので便乗。めんどくさい所はるびマに任せます!!
※ Turnip についてのドキュメント、一応職場の開発チームだけが見える場所に書いてあるんですが 認識が間違ってたら誰かに教えてもらうためにここにも書いておきます。 だれか Turnip 使い込んでる人いろいろ教えてください!!
本稿のターゲット
- Cucumber 使ったことある
- 使ったことなくても知っている
- 知らなくてもとりあえず見てみたい
- Turnip 、まずは動かしてみたい
という人
Turnip とは
Turnip とは、RSpec 向けの Gherikin 拡張ライブラリです(直訳)。詳しくは るびま で!
「Cucumber みたいなもの」って言えば最近は通じると思います。
まずはやってみよう、の前の準備
Ruby 1.9 系以上を入れてください。入れたあとは
$ mkdir turnip_example
$ cd turnip_example
$ mkdir spec
とかしてディレクトリ掘って、以下のファイルを作ってください。
$ $EDITOR Gemfile
source 'https://rubygems.org' gem 'turnip'
$ $EDITOR .rspec
-r turnip
$ $EDITOR spec/spec_helper.rb
require 'rubygems' require 'bundler/setup' Dir.glob("spec/steps/**/*steps.rb") { |f| load f, true }
作ったあとは
$ bundle install --path vendor/bundle
Fetching gem metadata from https://rubygems.org/..........
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Installing diff-lcs (1.2.4)
Installing multi_json (1.7.3)
Installing gherkin (2.12.0)
Installing rspec-core (2.13.1)
Installing rspec-expectations (2.13.0)
Installing rspec-mocks (2.13.1)
Installing rspec (2.13.0)
Installing turnip (1.1.0)
Using bundler (1.3.5)
準備完了です!! Turnip を試す最小構成ができあがりました。
まずはやってみよう
シナリオ作成
BDD なので(?)、テストしたいシナリオがあるはずです。シナリオを書きましょう。
$ $EDITOR spec/features/battle.feature
Feature: 戦闘 Scenario: 柔い敵と戦闘 Given "スライム" が現れた! When 勇者の攻撃! Then モンスターを倒した Scenario: 固い敵と戦闘 Given "はぐれメタル" が現れた! When 勇者の攻撃! Then モンスターを倒せなかった
- スライムは倒せる
- はぐれメタルは防御力高くて倒しきれない
そういうシナリオですね。
この文法は冒頭で述べた Gherikin と呼ばれる DSL のものです。 Cucumber 使ってる人にはおなじみでしょう。 詳しい中身は Cucumber のフィーチャの文法 - Gherkin - そんなこと覚えてない がまとめられてて良いです。
さあ実行してみましょう。
$ bundle exec rspec ** Pending: 戦闘 柔い敵と戦闘 "スライム" が現れた! -> 勇者の攻撃! -> モンスターを倒した # No such step: '"スライム" が現れた!' # ./spec/features/battle.feature:68 戦闘 固い敵と戦闘 "はぐれメタル" が現れた! -> 勇者の攻撃! -> モンスターを倒せなかった # No such step: '"はぐれメタル" が現れた!' # ./spec/features/battle.feature:68 Finished in 0.00162 seconds 2 examples, 0 failures, 2 pending
これまた冒頭で述べた通り Turnip は RSpec 向けの拡張(直訳)ですので、テスト実行も RSpec で行います*1。
(Turnip 仕様補足) feature ファイル
rspec
は引数無しで実行すると、通常は ./spec/**/*_spec.rb
ファイルを実行します(多分)。
ですが -r turnip
が呼ばれている場合は ./spec/**/*.feature
ファイルも走査対象に含めます。
ステップ作成
先程の rspecの結果は 2 Pending になりました。シナリオは用意してありますが、その中身が実装されていない為です。 それでは中身を書いていきましょう。
$ $EDITOR spec/stpes/battle_steps.rb
# coding: utf-8 MONSTERS = [ ['スライム', 2], ['はぐれメタル', 500] ] step ":monster が現れた!" do |name| @defence = MONSTERS.assoc(name).last end step "勇者の攻撃!" do @defence -= 3 end step "モンスターを倒した" do expect(@defence).to be <= 0 end step "モンスターを倒せなかった" do expect(@defence).to be > 0 end
RSpec 向けの拡張(直訳)ですので、RSpec Matcher も問題なく使えます。
ステップファイルを書く時に注意するのは、ファイル名が spec_helper.rb
に記述した Dir.glob("spec/steps/**/*steps.rb")
の範囲にいることです。Turnip ではテスト実行前にステップファイルが load されていないといけないからです*2。
というわけでもう一回実行しましょう。
$ bundle exec rspec .. Finished in 0.00527 seconds 2 examples, 0 failures
全 pass しました。おめでとうございます。
step ブロック
Turnip と Cucumber との違いの一つが、ステップ定義の仕方です。
Cucumber
Given /^"(.*)" が現れた!$/ do |name| end
Turnip
step ":monster が現れた!" do |name| end
- Cucumber
- ステップキーワード + 正規表現
- 引数は後方参照用グループで指定
- Turnip
step
+ 文字列- 引数は文字列中に「:キーワード」を埋め込んで指定
Turnip の作者である @jonicklas さんは、Cucumber の問題の一つにこのステップ定義を挙げていました。それを解消する手段として Turnip を作った的な感じです。
Mapping steps to regexps is hard
(補足) キーワードの指定について
Cucumber では「When」や「Then」など、Turnip では「step」でブロックを呼び出しますが、 feature ファイルとは特に合っていなくても問題はありません。
When /^"(.*)" が現れた!$/ do |name| end or Then /^"(.*)" が現れた!$/ do |name| end
step ":monster が現れた!" do |name| end
Scenario: 柔い敵と戦闘 Given "スライム" が現れた! # <= When でも Then でもないけど問題なし
とりあえずステップ名とマッチしていればキーワードはどうでもいい、みたいな感じらしいです。
極端な例でいくと
Feature: 戦闘 Scenario: 柔い敵と戦闘 And "スライム" が現れた! And 勇者の攻撃! And モンスターを倒した Scenario: 固い敵と戦闘 And "はぐれメタル" が現れた! And 勇者の攻撃! And モンスターを倒せなかった
このような形で全て "And" でもテストそのものは実行されます。 これに関しては るびま でも触れているように、「なんでもいいんだけどせっかく仕様を書くんだからそのステップに合ったキーワードを書いた方が見やすいよね」っていう感じです。
(Turnip 仕様補足) インスタンス変数の扱い
さきほどのステップ、@defence
とかみたいにインスタンス変数を使えるのはなんでかっていうと、*.feature
ファイルに書いた Scenario 毎に
RSpec::Core::ExampleGroup::Nested_1::Nested_1
RSpec::Core::ExampleGroup::Nested_1::Nested_2
という感じで、RSpec でおなじみの ExampleGroup が割り当てられます(ちなみにネストしているのは Feature
、Scenario
の分ですね)。
だから step do ; end
の中は ExampleGroup クラス内でできることは大概できます。例えば
step "モンスターを倒した" do
expect(@defence).to be <= 0
+ puts example.metadata.keys
end
とかすると
$ bundle exec rspec example_group type turnip example_group_block description_args caller execution_result file_path .. Finished in 0.00571 seconds 2 examples, 0 failures
みたいな感じですね。
また、先程述べたとおり、Scenario 毎に ExampleGroup が割り当てられるため、これらのインスタンス変数はそのシナリオ内でのみ保持されます。なので同じステップを違うシナリオから呼び出しても値がごっちゃになることはないので安心して節度をもって使って下さい。
長くなってきたのでここまで
まだ続きます。多分