Cucumberでブログシステムの統合テストをする
バイト先の社内勉強会でCucumberのデモをやったときのログです。
はじめに
このエントリーではCucumberのセットアップから新しいシナリオの追加までの手順を紹介します。エントリーを管理するシンプルなRailsアプリケーションに対するCucumberによるテストを生成し、Feature、Stepの読解とエントリーの変更シナリオの追加を行います。
セットアップ
こちらを参考に。
インストール
$ sudo apt-get install libxml2-dev libxslt1-dev
必要なRubyのライブラリをインストールします。
$ sudo gem install nokogiri rspec rspec-rails webrat cucumber
ブログシステムを作成
この間作ったマスターは使いません。
タイトルと本分を持つエントリーを管理するシンプルなscaffoldを作成します。
$ rails blog $ cd blog/ $ script/generate scaffold entry title:string body:text $ rake db:migrate $ rake db:migrate RAILS_ENV=test
Cucumberの準備
railsアプリでCucumberを使う準備をします。
$ script/generate cucumber create features/step_definitions create features/step_definitions/webrat_steps.rb create features/support create features/support/env.rb create features/support/paths.rb exists lib/tasks create lib/tasks/cucumber.rake create script/cucumber
シナリオを作成する際に使用する語彙やrakeタスクが生成されます。
エントリー管理のシナリオテストを生成
$ script/generate feature entry title:string body:text exists features/step_definitions create features/manage_entries.feature create features/step_definitions/entry_steps.rb
エントリー管理をテストするための、manage_entries.featureとentry_steps.rbが生成されます。
CucumberではFeatureという自然言語のシナリオと、それをRubyで解釈するためのStepの二つが登場します。詳しくは、Cucumberがアツい - moroの日記
Featureを実行してみる
ジェネレータで生成されたFeatureを実行してみます。
$ rake features ... 2 scenarios 9 steps passed
2つのシナリオと9つのステップが実行されたらしい。では、シナリオがどのようになっているのか見てみましょう。
features/manage_entries.feature
Feature: Manage entries In order to [goal] [stakeholder] wants [behaviour] Scenario: Register new entry Given I am on the new entry page When I fill in "Title" with "title 1" And I fill in "Body" with "body 1" And I press "Create" Then I should see "title 1" And I should see "body 1" Scenario: Delete entry Given the following entries: |title|body| |title 1|body 1| |title 2|body 2| |title 3|body 3| |title 4|body 4| When I delete the 3rd entry Then I should see the following entries: |title|body| |title 1|body 1| |title 2|body 2| |title 4|body 4|
Scenario:で始まるエントリー登録のシナリオとエントリー削除の2つのシナリオが生成されてますね!どう見てもただの英文テキスト。
Given, When, Then, Andで始まるのがStepでちょうど9つあります。
シナリオとステップの関係
シナリオはステップから構成されていて、シナリオごとに
- Given: 前提として○○、
- When: そのとき□□すると、
- Then: △△になるはず。
という検証を行っています。では、このシナリオはどのように解釈されているんでしょうか。
ここで、ステップ定義の登場です。
features/step_definitions/entry_steps.rb
Given /I am on the new entry page/ do visit "/entries/new" end Given /^the following entries:$/ do |entries| Entry.create!(entries.hashes) end When /^I delete the (\d+)(?:st|nd|rd|th) entry$/ do |pos| visit entries_url within("table > tr:nth-child(#{pos.to_i+1})") do click_link "Destroy" end end Then /^I should see the following entries:$/ do |entries| entries.raw[1..-1].each_with_index do |row, i| row.each_with_index do |cell, j| response.should have_selector("table > tr:nth-child(#{i+2}) > td:nth-child(#{j+1})") { |td| td.inner_text.should == cell } end end end
ステップに対するRubyの命令が書かれています。GivenやThenには/I am on the new entry page/のように正規表現が与えられていて、ステップがこの正規表現にマッチするとブロック内のRubyの命令が実行される仕組みです。
バグがあったときどうなるのか
バグがあるとどうなるのでしょう。
app/views/entries/show.html.erbで本文を出力しているところをコメントアウトしてみます。
app/views/entries/show.html.erb
@@ -5,7 +5,7 @@ <p> <b>Body:</b> - <%=h @entry.body %> + <%#=h @entry.body %> </p>
Featureを実行すると、
$ rake features Scenario: Register new entry # features/manage_entries.feature:6 ... And I should see "body 1" # features/step_definitions/webrat_steps.rb:89 expected: /body 1/m, got: "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n ... 2 scenarios 8 steps passed 1 step failed rake aborted!
I should see "body 1"というステップが落ちています。
なるほど、こんな感じで検証できるんですね。app/views/entries/show.html.erbを元に戻して次いってみましょう。
エントリー変更を検証するシナリオを追加する
エントリー登録のシナリオを参考にエントリー変更のシナリオを作成してみます。
シナリオはこんな感じになるはず。
- 前提として、id○番のエントリー変更ページを表示していて
- かつ、"title 1"と"body 1"が表示されている
- そのときTitleに"title updated"と入力して
- かつ、Updateボタンを押すと
- "title updated"と表示されるはず。
最初から日本語でFeatureを書いておけばよかったと思いつつ、今回は最後まで英語でいきます。
シナリオを追加します。
features/manage_entries.feature
... Scenario: Register new entry ... Scenario: Edit existing entry Given I am on the edit entry page of 1 And I should see "title 1" And I should see "body 1" When I fill in "Title" with "title updated" And I press "Update" Then I should see "title updated" ... Scenario: Delete entry
Featureを実行してみます。
$ rake features ... 3 scenarios 9 steps passed 5 steps skipped 1 step pending (1 with no step definition)
1 step pendingということで、新しく追加したGiven I am on the edit entry page of 1 というシナリオがペンディングになっているはずです。解釈できないステップがあるとペンディングになるんですね。
ステップ定義を追加しましょう。idの部分は可変なので変数にします。正規表現の後方参照を使うとブロック引数として受け取ることができます。
features/step_definitions/entry_steps.rb
Given /I am on the edit entry page of (\d+)/ do |id| visit "/entries/#{id}/edit" end
Featureを実行します。
rake features ... Scenario: Edit existing entry # features/manage_entries.feature:14 Given I am on the edit entry page of 1 # features/step_definitions/entry_steps.rb:5 Couldn't find Entry with ID=1 (ActiveRecord::RecordNotFound) ... 3 scenarios 9 steps passed 1 step failed 5 steps skipped rake aborted!
あれ…落ちましたね。原因はActiveRecord::RecordNotFound。初期データが足りてないんですね。削除のシナリオと同じように初期データを用意しましょう。
最終的なFeatureはこうなります。
features/manage_entries.feature
... Scenario: Register new entry ... Scenario: Edit existing entry Given the following entries: |id|title|body| |1|title 1|body 1| Given I am on the edit entry page of 1 And I should see "title 1" And I should see "body 1" When I fill in "Title" with "title updated" And I press "Update" Then I should see "title updated" ... Scenario: Delete entry
今度こそ、Featureを実行すると、
$ rake features ... 3 scenarios 16 steps passed
通った!
試しに、間違ってEntryモデルのタイトルメソッドをオーバーライドすると…
app/models/entry.rb
class Entry < ActiveRecord::Base def title "title 1" end end
rake features ... Scenario: Edit existing entry # features/manage_entries.feature:14 Given the following entries: # features/step_definitions/entry_steps.rb:9 Given I am on the edit entry page of 1 # features/step_definitions/entry_steps.rb:5 And I should see "title 1" # features/step_definitions/webrat_steps.rb:89 And I should see "body 1" # features/step_definitions/webrat_steps.rb:89 When I fill in "Title" with "title updated" # features/step_definitions/webrat_steps.rb:18 And I press "Update" # features/step_definitions/webrat_steps.rb:10 Then I should see "title updated" # features/step_definitions/webrat_steps.rb:89 expected: /title updated/m, ... 3 scenarios 14 steps passed 2 steps failed
バッチリおちました!
まとめ
今回はscaffoldに対するシナリオテストを生成し、Cucumberの基本的な使い方を見ていきました。ジェネレータで生成された英語のシナリオをそのまま使いましたが、日本語でシナリオを書くことも可能です。自然言語で書いたシナリオがそのまま動くなんて動く仕様書も夢じゃない!