Cucumberがアツい

仕事で作っているRailsアプリにCucumberを突っ込んでみました。これは熱い。いやもう十分、お客さんに見せて分かってもらえる気がします。たぶん。もちろん準備は必要だし、受け入れ仕様をすべてお客さんに書いてもらうというのは難しいですけど*1

とりあえず導入はこちらから。最近はNokogiriが必要です。あとTerminal.appで--no-colorつけずに実行するとTerminal.appがひどいことになるのでiTermお薦めです。

http://github.com/aslakhellesoy/cucumber/wikis/ruby-on-rails

2010-11-10

SEO的に。この記事を書いてから2年、いろんなCucumberの使い方を調べました。そのノウハウを達人出版会にて本にまとめました。よろしければこちらもどうぞ。

http://tatsu-zine.com/books/cuke

使い方

rails部門のtophatenarらしいので(あんまり関係ないけど)Rails前提の話しです。

cucumberプラグインをインストールして、script/generate cucumberすると、Railsをテストするためのいろんなファイルが生成されます。たとえばscript/cucumberに実行スクリプトが入ります。

差しあたり重要なのは以下の二つほど。

  • features/support/env.rb
    • cucumber全体の設定を各ファイルです。最初は編集しなくていいでしょう。
  • features/step_definitions/webrat_steps.rb
    • webratという、Webアプリのテスト用ツールを使うためのコードです。これも編集しなくていいはず。
cucumberの構造

cucumberでは、まずfeatures/hoge.featureという命名規則でfeatureファイルを書きます。これが「自然言語(ぽい)」「プレーンテキスト」で「受け入れ仕様を記述する」ファイルとなります。

たとえば次のようなコマンドで受け入れ仕様のひな形を作ります。

$ ruby script/generate feature Entry
      exists  features/step_definitions
      create  features/manage_entries.feature
      create  features/step_definitions/entry_steps.rb

ここでできたfeatures/manage_entries.featureがfeatureファイルですね。中身はこんなの。

$ cat features/manage_entries.feature 
Feature: Manage entries
  In order to keep track of entries
  A entry mechanic
  Should be able to manage several entries
  
  Scenario: Register new entry
    Given I am on the new entry page
    And I press "Create"

  Scenario: Delete entry
    Given there are 4 entries
    When I delete the first entry
    Then there should be 3 entries left
    
  More Examples:
    | initial | after |
    | 100     | 99    |
    | 1       | 0     |

どうみてもプレーンテキストですね。すばらしい。これを実行できるのがCucumberです。

で、どうみてもプレーンテキストなので、このままでは実行できません。これをなんとかして実行するための仕組みがstepsと呼ばれているものです。具体的には features/step_definitions/entry_steps.rb ですね。

$ cat features/step_definitions/entry_steps.rb 
Given /I am on the new entry page/ do
  visits "/entries/new"
end

Given /there are (\d+) entries/ do |n|
  Entry.transaction do
    Entry.destroy_all
    n.to_i.times do |n|
      Entry.create! :name => "Entry #{n}"
    end
  end
end

When /I delete the first entry/ do
  visits entries_url
  clicks_link "Destroy"
end

Then /there should be (?d+) entries left/ do |n|
  Entry.count.should == n.to_i
  response.should have_tag("table tr", n.to_i + 1) # There is a header row too
end

ということでfeatureファイルの文言にマッチするような正規表現と、実際のRubyコードが並んでいます。具体的にはインデントレベル2くらいのGivenやAnd、When、Thenの文言に対応しています。

Cucumberは実行時にfeatureファイルを解析し、文言がマッチするstepの処理を実行してテストします。stepは正規表現を使えますので、文章中の一部を動的に取り出すことも出来ます。たとえば "Given there are 4 entries"というfeature内の文言に対し、Given /there are (\d+) entries/ というstepがマッチして、"4"を動的に指定できているイメージが伝わるはず。

で、Given、When、Thenはそれぞれ、前提、処理内容、その結果(に対する検証)、となります。これを上から順に書いていくわけですね。Andは前のstepと同じ事を繰り返します。Thenでは検証をするんですが、検証にはRSpecの語彙(should/should_not)が使えます。

また、このシナリオを日本語で書いてみればこんな感じです。

  Scenario: 新しいエントリの登録
    前提 I am on the new entry page
    かつ I press "Create"

  Scenario: エントリの削除
    前提 there are 4 entries
    もし I delete the first entry
    ならば there should be 3 entries left

さらに、"there are 4 entries"とかのところはstepで正規表現で指定してます。そのためstepに次のような定義があれば

Given /^(\d+).? のエントリがある/ do |n|
  Entry.transaction do
    Entry.destroy_all
    n.to_i.times do |n|
      Entry.create! :name => "Entry #{n}"
    end
  end

こう書けます。

  Scenario: エントリの削除
    前提 4つ のエントリがある
    もし I delete the first entry
    ならば there should be 3 entries left

こんな感じでstepを充実させていけばOK。

と、ここで終わると面倒なだけじゃんと思うかもしれませんが、意外とテストに書く語彙(=やること)は決まってるじゃないですか。こうExcelでテスト仕様書書いてても。なので、一回つくってしまえば意外と使い回せそうな気がしてます。

で、実はここで書いた「前提」とかは角谷さんの尽力によりそのまま使えます。

http://github.com/aslakhellesoy/cucumber/commit/9291e7c8d1de8189dfda3d2d7318277731fb9c5b

ということでもう少し詳しい使い方はあとで書く。

Railsとの絡み

箇条書きで。

  • Railsのテストをするためにwebratを使ってる
  • webratでHTMLを検証するためにNokogiri使ってる
  • RailsのテストはIntegrationTestのレイヤで。ActionController::Integration::Sessionを使ってます。
    • ようはActionController::Dispatcher.dispach から先を検証している
    • これをwebratのSessionでくるんで使う感じ。
  • IntegrationTestの制約は当然受ける
    • どのリダイレクト先もテスト対象のRailsアプリに行くとか。
    • おかげでOpenIDで苦労したし。
  • fixturesも使えるはず

OpenIDの件はあとで書く、ということで私のTwitterをみてwktkしてた人にはひどいことをしましたよね。ごめんなさい。あとで書きます。

*1:そもそも*すべて*お客さんに書いてもらうのをあんまり目指していない。ちゃんとシステムをみながら会話する語彙としては使えそう