3 Feb 2010

Cucumber入門(1) 一番最初のCucumber

※. Javascript版のGoogle Searchに対応しました 21, Apl, 2012

明日Cucmberについてのmeetupがあるので予習します。 Cucumberとは
Cucmberはプレーンなテキストの機能記述を自動テストとして実行するツールです。Cucumberが理解する言語はGherkinというものです。 CucumberそれそのものはRubyで書かれていますが、Rubyやその他の言語(java、C#とpaythonを含みそれだけではない)で書かれたコードを”テスト”するために使われます。Cucumberは最小限のruby programmingを必要とし、Rubyは簡単です。なのでRubyでないコードを開発していても恐れないでください。 githubより
RailsにCucumberを導入した状態から始めようと思ったのですが、チュートリアルを読み進めていくうちにCucumberを構成するコンポーネントに選択肢がいくつかありCucumber自体規模が大きいことに気づいたので、まずは入門編ということでCucumberをrailsと組み合わせないで動作させてみることにしました。Cucumberの構成については後日また記事にしようと思います。 ちょうど良い記事があったので失礼して拝借させていただきます。 Getting started with Cucumber, RSpec, Webrat and multiruby


Cucumberでは以下のような感じでQAが書いたシナリオ試験や、仕様の振る舞いをGerhkinのフォーマットで自然言語で定義します。

簡単に自然言語で書かれたシナリオ試験をテストコードまでに落とすフローを示します。


1. Gerhkinのフォーマットで書かれたシナリオ - イメージをつかんでもらうための便宜上日本語にしました

主な特徴:  Google 検索
  自分のドキュメントがGoogleで検索できるようになているか確認したい。
 
  シナリオ: 
    条件  "https://www.google.com/" 
    場面  "engineerflies" を検索する
    そこで "Cucumber"と表示されている "/url?q=http://engineerflies.blogspot.com" というリンクがあるはず


2. シナリオの自然言語に正規表現でマッチするような、Webrat(首なしブラウザ) で実施するテストを用意してあげる
条件 /^"([^\"]*)" を開く$/ do |url|  
 visit url  
end  
  
場面 /^"([^\"]*)" を検索する$/ do |query|
  fill_in "q", :with => query 
  click_button "Google 検索"
end   
 
そこで /^"([^\"]*)"と表示されている"([^\"]*)" というリンクがあるはず$/ do |url, text|
  response_body.should have_selector("a[href^='#{ url }']") do |element|
    element.should contain(text)
  end
end



それでは今回はgoogleでこのブログを検索してヒットするまでをcucumberで検証してみたいと思います。今回のCucmberの構成は以下のとおり。Googleを使うのでWebratを使用します。

Cucmber + Webrat + Mechanize


* Webratの代わりにCapybaraという選択肢も考えましたが、CapybaraはgithubのdescriptionにあるようにRackアプリのテストを前提としているので今回は使用しません。Railsの外で使用する場合、SinatraやMerbで使用する場合は Capybara.app = MyRackApp のようにRackのアプリケーションを渡してあげる必要があります。

Cucmberのインストール

私の環境では次のバージョンをインストールしました
 #gemのインストール
sudo gem install cucumber -v 0.6.2
sudo gem install webrat -v 0.6.0
sudo gem install mechanize -v 1.0.0
sudo gem install rspec



下準備

- Cucumberに必要なディレクトリとファイルの作成
どこでもいいので以下のディレクトリ構成を作成します。
 $ mkdir -p cucumber/features #cucumberが実行するのに必要なテストスウィートを用意する場所
 $ cd cucumber/features
 $ touch search.feature #後で説明するfeatureを定義するファイル
 $ mkdir step_definitions #?
 $ mkdir support #環境設定ファイル
 $ vi support/env.rb
require 'webrat'

Webrat.configure do |config|
  config.mode = :mechanize # Mechanizeを使ってブラウジングする
end

World(Webrat::Methods)
World(Webrat::Matchers)
とりあえずその状態でcucumberを実行する。
 $ cd cucumber
 $ cucumber
   0 scenarios0 steps
   0m0.000s
当然ながらscenarioもstepも無い。scenarioとstepは以下を参照

Feature

まずfeatures/search.featureに以下を記述。
Feature: Search Google
  In order to make sure people can find my documentation
  I want to check it is listed on the first page in Google

  Scenario: Searching for suzukimilanpaak engineerflies
    Given I have opened "https://www.google.com/"
    When I search for "suzukimilanpaak engineerflies"
    Then I should see a link to "/url?q=http://engineerflies.blogspot.com" with text "Cucumber"
- Cucumberの便宜上の単位
  • Feature: 主要なもの、目玉。
  • Scenario: 人間が行う操作のまとまり
  • Step: 人間が行う操作や結果の最小単位
- 数の関係 一つの.featureファイルに複数のFeatureを書いたら Cucumber::Parser::SyntaxError になりました

単:Feature - 復:Scenario - 復:Step


- Stepの種類
  • Given~: ~がという状況が与えられている
  • When~: ~したとき
  • Then~: それで~なる(はず)


またcucumber実施
 $cucumber
Feature: Search Google
  In order to make sure people can find my documentation
  I want to check it is listed on the first page in Google

  Scenario: Searching for engineerflies                                                         # features/search.feature:5
    Given I have opened "https://www.google.com/"                                               # features/search.feature:6
    When I search for "suzukimilanpaak engineerflies"                                           # features/search.feature:7
    Then I should see a link to "/url?q=http://engineerflies.blogspot.com" with text "Cucumber" # features/search.feature:8

1 scenario (1 undefined)
3 steps (3 undefined)
0m0.007s

You can implement step definitions for undefined steps with these snippets:

Given /^I have opened "([^"]*)"$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

When /^I search for "([^"]*)"$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

Then /^I should see a link to "([^"]*)" with text "([^"]*)"$/ do |arg1, arg2|
  pending # express the regexp above with the code you wish you had
end
Scenarioとfeatureの各行末にfeatureの行番号が表示され、その後に結果が表示されます。
1 scenario (1 undefined)
3 steps (3 undefined)
0m0.002s 1つのscenarioが未定義、3つのstepが未定義。まだ、テストを記述していないので当然の結果ですね。

その後に"You can implement step definitions for undefined steps with these snippets:"のメッセージが表示されてます。

仰せのとおりに...

Steps

Stepsを記述していきます。

Given

features/search_steps.rbを用意します
 
Given /^I have opened "([^\"]*)"$/ do |url|  
 visit url  
end  
  
When /^I search for "([^\"]*)"$/ do |arg1| 
  pending
end

Then /^I should see a link to "([^\"]*)" with text "([^\"]*)"$/ do |arg1, arg2| 
    pending  
end

再度cucumberを実行
Feature: Search Google
  In order to make sure people can find my documentation
  I want to check it is listed on the first page in Google

  Scenario: Searching for suzukimilanpaak engineerflies                                         # features/search.feature:5
    Given I have opened "https://www.google.com/"                                               # features/step_definitions/search_steps.rb:1
    When I search for "suzukimilanpaak engineerflies"                                           # features/step_definitions/search_steps.rb:5
      TODO (Cucumber::Pending)
      ./features/step_definitions/search_steps.rb:6:in `/^I search for "([^\"]*)"$/'
      features/search.feature:7:in `When I search for "suzukimilanpaak engineerflies"'
    Then I should see a link to "/url?q=http://engineerflies.blogspot.com" with text "Cucumber" # features/step_definitions/search_steps.rb:15

1 scenario (1 pending)
3 steps (1 skipped, 1 pending, 1 passed)
0m1.039s
結果を見てみましょう。 1 passed - Givenだけが通りました。


結果の見方
1 passed - Givenのstepが通った
1 pending - Whenはpendingが指定されている
1 skipped - Whenがpendingのため、Thenはskipされた



When

次にWhen にフォーカスします。
When /^I search for "([^\"]*)"$/ do |query|
  #qという名前のHTML要素にqueryを入力
  fill_in "q", :with => query 
  click_button "Google 検索"  
end   
 
Feature: Search Google
  In order to make sure people can find my documentation
  I want to check it is listed on the first page in Google

  Scenario: Searching for suzukimilanpaak engineerflies                                         # features/search.feature:5
    Given I have opened "https://www.google.com/"                                               # features/step_definitions/search_steps.rb:1
Sorry, you need to install launchy to open pages: `gem install launchy`
    When I search for "suzukimilanpaak engineerflies"                                           # features/step_definitions/search_steps.rb:5
    Then I should see a link to "/url?q=http://engineerflies.blogspot.com" with text "Cucumber" # features/step_definitions/search_steps.rb:14
      TODO (Cucumber::Pending)
      ./features/step_definitions/search_steps.rb:15:in `/^I should see a link to "([^\"]*)" with text "([^\"]*)"$/'
      features/search.feature:8:in `Then I should see a link to "/url?q=http://engineerflies.blogspot.com" with text "Cucumber"'

1 scenario (1 pending)
3 steps (1 pending, 2 passed)
0m1.111s

Whenも通りましたね。うまく通らない場合はWhen の中でsave_and_open_pageでレスポンスの内容を作業ディレクトリに保存することができます。response_bodyにアクセスすることでGivenでvisit urlした結果、実際に返ってきたレスポンスを見ることができます。
When "..." do 
  save_and_open_page #=> ./webrat-1335366157.html

  # Above is equivalent to..
  File.open "webrat.html" do |f|
    f.puts response_body.inspect
  end
end
実際にwebratがどのようなリクエストをしているかは作業ディレクトリの./webrat.logにいかの様なけいしきで保存されます。
D, [2012-04-26T00:17:11.930393 #10569] DEBUG -- : REQUESTING PAGE: GET https://www.google.com/ with {} and HTTP headers {}
D, [2012-04-26T00:17:12.267081 #10569] DEBUG -- : REQUESTING PAGE: GET /search with {"btnG"=>"Google \346\244\234\347\264\242", "hl"=>"ja", "gbv"=>"1", "q"=>"suzukimilanpaak engineerflies", "ie"=>"Shift_JIS", "source"=>"hp"} and HTTP headers {"HTTP_REFERER"=>"https://www.google.com/"}


Then

 Then /^I should see a link to "([^\"]*)" with text "([^\"]*)"$/ do |url, text|
  #response bodyにはセレクター"a[href='#{ url }']"で指定した要素があるはず
  response_body.should have_selector("a[href='#{ url }']") do |element|
  #セレクターで取得された要素はtextを含んでいるはず
  element.should contain(text)
  end
 end
 
Feature: Search Google
  In order to make sure people can find my documentation
  I want to check it is listed on the first page in Google

  Scenario: Searching for suzukimilanpaak engineerflies                                         # features/search.feature:5
    Given I have opened "https://www.google.com/"                                               # features/step_definitions/search_steps.rb:1
Sorry, you need to install launchy to open pages: `gem install launchy`
    When I search for "suzukimilanpaak engineerflies"                                           # features/step_definitions/search_steps.rb:5
    Then I should see a link to "/url?q=http://engineerflies.blogspot.com" with text "Cucumber" # features/step_definitions/search_steps.rb:14

1 scenario (1 passed)
3 steps (3 passed)
0m1.292s
どうでしょう、3stepsとも通りましたでしょうか?


まとめ

以上のようにCucumberを使ったBDDでは、まずFeatureとScenarioを作成します。それから出来上がったScenarioをWebrat(首なしブラウザ)がブラウジングして結果をRspecの関数などでチェックします。このステップは自然言語であるGerhkinに正規表現でマッチするScenarioをどんどんプログラミングコードに落とすというようなことをすることになります。前者のステップまでをQA Scenario Writer、後者のステップからはQA Engineerが担当しRubyのコードをどんどん書くというふうに担当を分けることもできます。この時RubyをDSL(Domain Specific Language - ドメイン固有言語)と呼びます。実際のRubyでBDDを行っている現場では、簡易的なドメイン固有言語としてRubyのコードを最低限書けることというがQAの採用の条件になっていることがほとんどです。

Gerhkin編集専用のテキストエディタも出ていました。ずいぶんと一般化してきている印象 .