Cucumberでブログシステムの統合テストをする

バイト先の社内勉強会でCucumberのデモをやったときのログです。

はじめに

このエントリーではCucumberのセットアップから新しいシナリオの追加までの手順を紹介します。エントリーを管理するシンプルなRailsアプリケーションに対するCucumberによるテストを生成し、Feature、Stepの読解とエントリーの変更シナリオの追加を行います。

環境

セットアップ

こちらを参考に。

インストール

Debian/Ubuntuユーザーの人は先にこっちを。

$ sudo apt-get install libxml2-dev libxslt1-dev

必要なRubyのライブラリをインストールします。

$ sudo gem install nokogiri rspec rspec-rails webrat cucumber
依存

矢印の左が右に依存しています。

  • cucumber→webrat
  • webrat→rspecとnokogiri
  • nokogiri→libxmlとlibxslt

ブログシステムを作成

この間作ったマスターは使いません。

タイトルと本分を持つエントリーを管理するシンプルな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つあります。

シナリオとステップの関係

シナリオはステップから構成されていて、シナリオごとに

  1. Given: 前提として○○、
  2. When: そのとき□□すると、
  3. 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を元に戻して次いってみましょう。

エントリー変更を検証するシナリオを追加する

エントリー登録のシナリオを参考にエントリー変更のシナリオを作成してみます。
シナリオはこんな感じになるはず。

  1. 前提として、id○番のエントリー変更ページを表示していて
  2. かつ、"title 1"と"body 1"が表示されている
  3. そのときTitleに"title updated"と入力して
  4. かつ、Updateボタンを押すと
  5. "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の基本的な使い方を見ていきました。ジェネレータで生成された英語のシナリオをそのまま使いましたが、日本語でシナリオを書くことも可能です。自然言語で書いたシナリオがそのまま動くなんて動く仕様書も夢じゃない!