orangain flavor

じっくりコトコト煮込んだみかん2。知らないことを知りたい。

CucumberとJenkinsを使って、PHPなどで作られたリモートのWebアプリの受け入れテストを自動で行う

WordPressのようにPHPなどでできたWebサイトの振る舞いを自動でテストしたいと思い、仕組みを作ることにしました。

きっかけは、設定が間違ってて、トップページは正常に表示されているにも関わらず、個別のエントリページではエラーになってることがあったためです。

別にWordPressに限った話ではなく、Pythonとかで開発してても必要になる話なので、簡単なところから始めてみようというわけです。

なお、Rubyの世界をあまりわかってないので、Ruby周りの勉強を兼ねてます。間違っていたら教えてもらえるとありがたいです。

やりたいこと

  • ページがアクセスできるかチェックしたい。
  • なるべくサーバーとか立てずに簡単にやりたい。
  • 将来的にはちゃんとしたブラウザでの動作チェックに応用したい。

構成

今回の受け入れテストは、以下のライブラリを使って実現します。

  • Cucumber
  • Capybara
  • Capybara-Mechanize
Cucumberについて
Capybaraについて
  • Webアプリの振る舞いをテストするためのライブラリ。
  • かつてはWebratが主流だったけど、JavaScriptが使えないのでCapybaraに移行してるみたい。*1
  • ドライバ(Webアクセスする部分)を色々切り替えることができ、Selenium, Capybara-Webkitなどがある。
  • Rackアプリのテスト専用のrack_test(実際のHTTP通信をしない)がデフォルトのドライバ。
  • 今回は外部サイトをテストでき、簡単に使えるCapybara-Mechanizeを使う。
この構成にした理由
  • 最初はcurlで簡単にチェックしようと思ったけど、Jenkinsとの連携を考えるとちゃんとレポートを吐き出したい。
  • Java/Selenium WebDriver/HtmlUnit/JUnitという構成も試したけど、Javaの世界はなんだか面倒だった。というかSelenium WebDriverがあまり柔軟でないように見えた。*2
  • WebratはJavaScriptのテストに対応してないので、将来を考えてCapybaraを使うことにした。
  • Capybaraのドライバーは、簡単さからmechanizeを選択した。*3

前提

  • 開発環境に、Ruby, RubyGems, Bundler がインストールされている。
  • 自動ビルド環境にJenkinsがインストールされている。

Cucumberによる受け入れテストを実現するための手順

とりあえずJenkinsは置いておき、開発環境の上でテストを動かします。

1. Gemfileを作成する
$ bundle init

を実行するとカレントディレクトリに Gemfile ができるので、以下のように書き加えます。

# A sample Gemfile
source "https://rubygems.org"

# gem "rails"
gem "cucumber"
gem "rspec"
gem "capybara-mechanize"
2. gemをインストールする
$ bundle install --path=vender/bundle

pathを指定しない場合はシステム環境にインストールされるので注意。

3. cucumberのディレクトリ構成を作る
$ mkdir features
$ mkdir features/step_definitions
$ mkdir features/support
4. cucumberの設定をする

features/support/env.rb を以下の内容で作成します。ここではテスト対象としてGoogleを指定していますが、本来は自分のWebサイトを指定します。

#coding: utf-8

require 'capybara'
require 'capybara/cucumber'
require 'capybara/mechanize'
require 'rspec'

# テスト対象のサイト
# ドライバがmechanizeの場合、他のサイトにもアクセスできる。
Capybara.app_host = 'http://google.co.jp'

# デフォルトで使われるドライバ
# これを指定しないと、rack_testが使われるため
# PHPなどRack以外のアプリはテストできない。
Capybara.default_driver = :mechanize

Before do
	# タイムアウトの設定(おそらくmechanizeでしか有効でない)
	# 時間がかかりすぎるときはエラーにする
	
	# コネクションを開くまでのタイムアウト秒数
	Capybara.current_session.driver.browser.agent.open_timeout = 1
	# データ読み出しのタイムアウト秒数
	Capybara.current_session.driver.browser.agent.read_timeout = 1
end

env.rbはテストに必要な設定をするファイルです。*4

読み込みに時間がかかるときはエラーにしたいので、タイムアウトの設定*5を探したのですが見つからなかったため、Mechanizeオブジェクトのタイムアウトを直接設定しています。いい方法をご存じの方は教えていただけると嬉しいです。

5. cucumberを実行する
$ bundle exec cucumber
0 scenarios
0 steps
0m0.000s

まだなにもテストケースを作っていないので、このように表示されます。
ちなみにbundle exec はコマンドを実行する際にローカルのgemを使うためのコマンドです。

6. featureを書く

features/browsing.featureを以下の内容で作成します。

# language: en

Feature: Search
     Scenario: Search for orangain 
          Given I am on the "/" page of Google
          When I search for "orangain"
          Then I should get response with content-type "text/html"
          And I should see "orangain - Google 検索" in the title bar
          And I should see "かと (orangain) on Twitter" in the page

featureには期待される振る舞いを(一定の規則に沿った)自然文で記述します。
featureは日本語でも書けますが、英語のほうが入力が楽そうなので英語にしました。
なお、Capybara-MechanizeのREADMEには@mechanizeを書くとありますが、env.rbで設定しているので不要です。

7. cucumberを実行する
$ bundle exec cucumber
# language: en
Feature: Search

  Scenario: Search for orangain                              # features/search.feature:4
    Given I am on the "/" page of Google                     # features/search.feature:5
    When I search for "orangain"                             # features/search.feature:6
    Then I should get response with content-type "text/html" # features/search.feature:7
    And I should see "orangain - Google 検索" in the title bar # features/search.feature:8
    And I should see "かと (orangain) on Twitter" in the page  # features/search.feature:9

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

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

Given /^I am on the "(.*?)" page of Google$/ 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 get response with content\-type "(.*?)"$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

Then /^I should see "(.*?)" in the title bar$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

Then /^I should see "(.*?)" in the page$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

こんどはスニペットが表示されます。便利。

8. step_definitionを作成する

features/step_definitions/search_steps.rb に先ほど表示されたスニペットを貼りつけて保存します。

#coding: utf-8

Given /^I am on the "(.*?)" page of Google$/ 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 get response with content\-type "(.*?)"$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

Then /^I should see "(.*?)" in the title bar$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

Then /^I should see "(.*?)" in the page$/ do |arg1|
  pending # express the regexp above with the code you wish you had
end

step_definitionsには、featureの自然文に相当するコード(step_definition)を書きます。正規表現で一致したstep_definitionが使われるというわけです。

9. cucumberを実行する
$ bundle exec cucumber
# language: en
Feature: Search

  Scenario: Search for orangain                              # features/search.feature:4
    Given I am on the "/" page of Google                     # features/step_definitions/search_steps.rb:3
      TODO (Cucumber::Pending)
      ./features/step_definitions/search_steps.rb:4:in `/^I am on the "(.*?)" page of Google$/'
      features/search.feature:5:in `Given I am on the "/" page of Google'
    When I search for "orangain"                             # features/step_definitions/search_steps.rb:7
    Then I should get response with content-type "text/html" # features/step_definitions/search_steps.rb:11
    And I should see "orangain - Google 検索" in the title bar # features/step_definitions/search_steps.rb:15
    And I should see "かと (orangain) on Twitter" in the page  # features/step_definitions/search_steps.rb:19

1 scenario (1 pending)
5 steps (4 skipped, 1 pending)
0m0.005s

今度はpendingって表示されます。便利。

10. step_definitionの中身を書く

features/step_definitions/search_steps.rb の pending となっていたところを実装します。

#coding: utf-8

Given /^I am on the "(.*?)" page of Google$/ do |url|
  visit url
end

When /^I search for "(.*?)"$/ do |query|
  fill_in 'q', :with => query
  click_button 'Google 検索'
end

Then /^I should get response with content\-type "(.*?)"$/ do |content_type|
  page.response_headers['Content-Type'].should match('^' + Regexp.escape(content_type))
end

Then /^I should see "(.*?)" in the title bar$/ do |title|
  page.should have_selector('title', :text => title)
end

Then /^I should see "(.*?)" in the page$/ do |content|
  page.should have_content(content)
end

should っていう不思議な感じの書き方は RSpec のものらしいです。

11. cucumberを実行する
$ bundle exec cucumber
# language: en
Feature: Search

  Scenario: Search for orangain                              # features/search.feature:4
    Given I am on the "/" page of Google                     # features/step_definitions/search_steps.rb:3
    When I search for "orangain"                             # features/step_definitions/search_steps.rb:7
    Then I should get response with content-type "text/html" # features/step_definitions/search_steps.rb:12
    And I should see "orangain - Google 検索" in the title bar # features/step_definitions/search_steps.rb:16
    And I should see "かと (orangain) on Twitter" in the page  # features/step_definitions/search_steps.rb:20

1 scenario (1 passed)
5 steps (5 passed)
0m0.950s 

成功したはずです。

もちろん様々な外部要因*6によって失敗するかもしれませんが、その場合は適当に直してみてください。

ここまでのソースコードorangain/testing_remote_webapps · GitHub にあります。

テストをJenkinsで自動化する手順

続いて、上で作ったテストをJenkinsに自動実行させます。

1. RVMプラグインをインストールする

Jenkinsの管理画面からRVM Pluginをインストールします。これは、RVMの環境でビルドを実行するためのプラグインで、RVM自体も自動でインストールしてくれます。

今回、Gemの管理はBundlerで行っているので、rbenv で十分な気がしましたが、rbenv pluginよりRVMプラグインのほうが実績があったので、こちらを選択しました。

2. 必要なパッケージをインストールする

RVMのインストール、Rubyコンパイル、Nokogiriのコンパイルなどで色々足りないと怒られるので、先に以下のパッケージをインストールしておきます。

sudo apt-get install curl
sudo apt-get install zlib1g-dev
sudo apt-get install libssl-dev
sudo apt-get install libxml2-dev
sudo apt-get install libxslt1-dev
3. Jenkinsのジョブを作成する

以下の設定をしたジョブを作成します。





プロジェクト名なにか適当に入力します。
ソースコード管理システムGitを選択し、Repository URLに以下のURLを入力します。

https://github.com/orangain/testing_remote_webapps.git

ビルド環境Run the build in a RVM-managed environmentにチェックを付け、Implementationに1.9.3と入力します。
ビルド「シェルの実行」を追加し、以下のコマンドを入力します。

gem install bundler
bundle install --path=vender/bundle
bundle exec cucumber --format junit --out reports

ビルド後の処理JUnitテスト結果の集計」を追加し、テスト結果XMLreports/*.xmlと入力します。

詳しくは設定画面のキャプチャをご覧ください。

4. ビルドを実行する

「ビルド実行」ボタンをクリックすると、ビルドが始まり以下のようにテスト結果が表示されるはずです。

手順は以上です。後はテストケースを追加し、post-updateフックで実行するなり、定期的に実行するなりご自由にどうぞ。

おまけ:専用ツールとの比較

後から知りましたが、Canoo WebTestのような専用ツールでやれば良かったかもしれません。ただ、今回作った仕組みは以下の2点に意味があると思います。

  • Cucumberのテストケース(feature)は自然文で書かれていて、非エンジニアにも読みやすい。
  • Capybaraは、シミュレータでのテストから本物のブラウザでのテストに変更したくなったときに、ドライバを変更するだけで対応できる。

環境

この記事は以下の環境に基づいています。

開発環境
  • Mac OS X Lion 10.7.5
  • ruby 1.8.7 (2012-02-08 patchlevel 358) [universal-darwin11.0]
  • gem 1.3.6
自動ビルド環境
  • Debian 6.0.5
  • Jenkins 1.489
  • RVM Plugin 0.3
  • ruby 1.9.3p327 (2012-11-10 revision 37606) [x86_64-linux]
  • gem 1.8.24
Gem
  • bundler (1.2.1)
  • capybara (1.1.3)
  • capybara-mechanize (0.3.0)
  • cucumber (1.2.1)

*1:The Ruby Toolbox - Browser testingって初めて知ったけど便利。

*2:HTTPのヘッダのテストとかできるのかな?

*3:JavaScriptのテストをするならCapybara-Webkitがいいらしい。

*4:ドライバを自由に使い分けたい場合は、素晴らしいenv.rbを公開してくださってる方がいたので、使わせてもらうと良いかもしれません。Testing remote (PHP) websites with Capybara, Cucumber, Mechanize, Selenium 2 Webdriver … and SauceLabs | Otaqui.com

*5:Capybara.default_wait_timeはそれっぽい名前ですが、Ajaxの処理を待つ時間なので関係有りません。

*6:GoogleのUIが変わったり、Twitterのタイトルが変わったり、検索順位が変わったり、インターネットに繋がってなかったり。