Thanks Driven Life

日々是感謝

Turnip について (1) / まずは動かす

Rubyist Magazine - エンドツーエンドテストの自動化は Cucumber から Turnip へ が出てたので便乗。めんどくさい所はるびマに任せます!!

※ Turnip についてのドキュメント、一応職場の開発チームだけが見える場所に書いてあるんですが 認識が間違ってたら誰かに教えてもらうためにここにも書いておきます。 だれか Turnip 使い込んでる人いろいろ教えてください!!

本稿のターゲット

  • Cucumber 使ったことある
  • 使ったことなくても知っている
  • 知らなくてもとりあえず見てみたい
  • Turnip 、まずは動かしてみたい
    • るびまの方では rails や capybara との連携も含めてがっつり書いているので、ここでは本当に 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 モンスターを倒せなかった
  1. スライムは倒せる
  2. はぐれメタルは防御力高くて倒しきれない

そういうシナリオですね。

この文法は冒頭で述べた 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 の作者である @ さんは、Cucumber の問題の一つにこのステップ定義を挙げていました。それを解消する手段として Turnip を作った的な感じです。

Mapping steps to regexps is hard

http://www.elabs.se/blog/30-solving-cucumber-s-problems

(補足) キーワードの指定について

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 が割り当てられます(ちなみにネストしているのは FeatureScenario の分ですね)。 だから 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 が割り当てられるため、これらのインスタンス変数はそのシナリオ内でのみ保持されます。なので同じステップを違うシナリオから呼び出しても値がごっちゃになることはないので安心して節度をもって使って下さい。

長くなってきたのでここまで

まだ続きます。多分

*1:詳しくは 本家README 「Usage」 をご覧下さい

*2:詳しくは 本家README 「Where to place steps」 をご覧下さい