Ruby on Railsアプリ以外でも Selenium RC を使えば RSpec でテストが書ける!
RSpec のテストを記述するDSLとしては素晴らしさは、 Selenium RC と組み合わせる事で Ruby on Railsアプリ以外でも使えます。
Selenium は Javascriptを使いWebアプリのテストを行うツールとして有名ですが、Selenium RC を使うと RSpecで書いたテストを Webブラウザー上で走らせる事が出来ます。
準備
Seleniumのページから Selenium IDE 、 Selenium RC (Remote Control) をダウンロードします。
Selenium IDEは Firefox上で動きますので Firefox を使っていない方は ダウンロード して下さい。最近 Firefox4.0がリリースされましたが、2011/3/28日現在は、旧版の 3.6 の方が良いかもしれません、Firefox3.6はここからダウンロードできます 。
Selenium IDE (selenium-ide-1.0.10.xpi) は Firefoxを起動しドラックしてインストールします(Firefoxの再起動が必要になります)。
また Selenium RC は java プログラムなので、実行するには JRE が必要です。
テストシナリオ作り
Selenium IDE を使うと、ブラウザー(Firefox)上での操作を記録しそれをテストとして再実行できます。Firefoxの ツールメニューに Selenium IDEがあるので選択すると 下の画像のような画面が表示されます。
右上の赤い丸ボタンが操作の記録ボタンで、起動時にONになっていますので、ここでテストの操作を行います。操作を行うと下の画像のように、操作がコマンドとして記録されます。
コマンドの意味は、下部のリファレンスに表示されます。まとまった情報は Seleniumドキュメント の Commonly Used Selenium Commands にあります。
コマンドの削除や追加変更がSelenium IDE上で行えますので、多少操作が間違ってもどんどん操作し、後から編集すれば良いと思います。操作が終わったら右上の赤い丸ボタンを押し記録を終了します。
また、テストシナリをファイルに保存したり、ファイルから読み込んだりできます。
以前書いた ここ や ここ のサンプルプログラムのテストを書いてみましょう。
コマンド | 対象 | 値 | |
---|---|---|---|
open | /todos/ | ||
clickAndWait | link=New Todo | ||
type | todo_task | Seleniumでテスト | |
clickAndWait | todo_submit |
このシナリオは /todos/ をアクセスし New Todo をクリックし、新規作成画面で Task に "Seleniumでテスト" を入力し、 Submitボタンを押しています。
Selenium では操作を行うだけではなく、画面に 指定された文字が表示されているか等を検証できます。下のスクリプトは先ほどのシナリオ終了時に表示画面に遷移するので、画面に Task: Seleniumでテスト という文字があるか検証しています。
コマンド | 対象 | 値 | |
---|---|---|---|
open | /todos/ | ||
clickAndWait | link=New Todo | ||
type | todo_task | Seleniumでテスト | |
clickAndWait | todo_submit | ||
assertText | css=p:nth-child(3) | Task: Seleniumでテスト |
assertText は画面(HTML)上の対象要素を指定し、その要素の文字を検証します。対象の指定は ID, name, Xpath, CSSセレクターなどで指定できます、ここではCSSセレクターで指定しています。詳細は ドキュメントのLocating Elements を参照して下さい。
Ajax部分のテストシナリオ作り
Ajaxを使ったアプリでも上の手順でテストシナリオを作成できますが、一つ注意点があります。Selenium は、いつAjaxによる画面操作が終わったか自動的には判定できないのです。
コマンド | 対象 | 値 |
---|---|---|
open | /todos/ | |
click | link=link=Show1 | |
assertText | css=#detail_area p:nth-child(3) | Task: 打ち合わせ |
と書けますが、これを実行すると assertText でエラーになります。ajaxの反応がある前に assertText が実行されてしまうからです。
そこで、以下のように ajax での変更が行われるまで待つコマンドを追加します。ここでは 画面に < div id=detail_area> ... < p> .. が表示されるのを待ってから assertText が実行されます。
コマンド | 対象 | 値 |
---|---|---|
open | /todos/ | |
click | link=link=Show1 | |
waitForElementPresent | css=#detail_area p | |
assertText | css=#detail_area p:nth-child(3) | Task: 打ち合わせ |
RSpecを出力させる
Selenium IDE が保存したテストシナリオは HTML のテーブルです。これを、まとめたりコメントを書いてメンテナンス性を上げる事はできますが、共通部分をまとめたり、条件を入れたりは出来ません。しかし Selenium IDE は下の画像の様に テストシナリオ を HTML以外の形式で出力できます。 Rubyist は当然 RSpec を選びます :-)
RSpec の実行
このRSpec を実行するには
- selenium-client のインストール sudo gem install selenium-client
- Selenium RC を別ターミナルで実行 java -jar selenium-server-standalone-2.0b3.jar
の準備を行い、RSpec形式で保存したファイルを RSpecで実行します。ブラウザーが立ち上がりテストが実行されます。
% spec -c -f s new_spec.rb
Selenium IDEが作る RSpec は RSpec 1.3 用ですが、以下のように少し変更すると RSpec 2.0 で実行できます。
require "rubygems" gem "rspec" gem "selenium-client" require "selenium/client" #require "selenium/rspec/spec_helper" ← コメントアウト、または削除 #require "spec/test/unit" ← コメントアウト、または削除 describe "test1" do attr_reader :selenium_driver alias :page :selenium_driver before(:all) do @verification_errors = [] @selenium_driver = Selenium::Client::Driver.new \ :host => "localhost", :port => 4444, :browser => "*chrome", :url => "http://localhost:3000/", :timeout_in_second => 60 end before(:each) do @selenium_driver.start_new_browser_session end after(:each) do # ← append_after を afterに変更 @selenium_driver.close_current_browser_session @verification_errors.should == [] end it "test_test1" do page.open "/todos/" page.click "link=Show1" !60.times{ break if (page.is_element_present("css=#detail_area p") rescue false); sleep 1 } ("Task: 打ち合わせ").should == page.get_text("css=#detail_area p:nth-child(3)") end end
RSpec のリファクタリング
上に書いた新規作成と Ajax表示の RSpec を1つにすると以下の様になります。
require "rubygems" gem "rspec" gem "selenium-client" require "selenium/client" describe "Todo管理" do attr_reader :selenium_driver alias :page :selenium_driver before(:all) do @verification_errors = [] @selenium_driver = Selenium::Client::Driver.new \ :host => "localhost", :port => 4444, :browser => "*chrome", :url => "http://localhost:3000/", :timeout_in_second => 60 end before(:each) do @selenium_driver.start_new_browser_session end after(:each) do @selenium_driver.close_current_browser_session @verification_errors.should == [] end it "新規作成でTodoが登録できる" do page.open "/todos/" page.click "link=New Todo" page.wait_for_page_to_load "30000" page.type "todo_task", "Seleniumでテスト" page.click "todo_submit" page.wait_for_page_to_load "30000" ("Task: Seleniumでテスト").should == page.get_text("css=p:nth-child(3)") end it "一覧で表示をクリックすると詳細が表示される" do page.open "/todos/" page.click "link=Show3" !60.times{ break if (page.is_element_present("css=#detail_area p") rescue false); sleep 1 } ("Task: Seleniumでテスト").should == page.get_text("css=#detail_area p:nth-child(3)") end end
clickAndWaitが page.click + page.wait_for_page_to_load 2行になっていたり、waitForElementPresent が文に展開されていて可読性が悪いですよね、そこで以下のようなヘルパーメソッドを追加
module Selenium module Client class Driver def click_and_wait_for_page_to_load(loc) click loc wait_for_page_to_load "30000" end def wait_for_element_present(loc) 60.times do return true if (is_element_present(loc) rescue false) sleep 1 end return false end end end end
これで、RSpec は以下のように可読性が上がりました ^^)
require "rubygems" gem "rspec" gem "selenium-client" require "selenium/client" require File.expand_path(File.dirname(__FILE__) + "/helper") describe "Todo管理" do attr_reader :selenium_driver alias :page :selenium_driver before(:all) do @verification_errors = [] @selenium_driver = Selenium::Client::Driver.new \ :host => "localhost", :port => 4444, :browser => "*chrome", :url => "http://localhost:3000/", :timeout_in_second => 60 @selenium_driver.close_current_browser_session end before(:each) do @selenium_driver.start_new_browser_session end after(:each) do @selenium_driver.close_current_browser_session @verification_errors.should == [] end it "新規作成でTodoが登録できる" do page.open "/todos/" page.click_and_wait_for_page_to_load "link=New Todo" page.type "todo_task", "Seleniumでテスト" page.click_and_wait_for_page_to_load "todo_submit" page.get_text("css=p:nth-child(3)").should == "Task: Seleniumでテスト" end it "一覧で表示をクリックすると詳細が表示される" do page.open "/todos/" page.click "link=Show3" page.wait_for_element_present "css=#detail_area p" page.get_text("css=#detail_area p:nth-child(3)").should == "Task: Seleniumでテスト" end end
テスト初期化処理の追加
テストを実行するには、サーバーアプリのデータベースを初期値に戻したりする操作が必要になります。そこで何らかの方法で初期化処理用のコントロラー(CGIなど)を追加し Seleniumから初期化できるようにすると良いと思います。
今回のサンプルは Railsなので以下のようなかなり手抜きのコントロラーを追加すると
class TestController < ApplicationController def setup system "rake db:fixtures:load" render :text => 'OK' end end
/test/setup に アクセスする事でデータベースの初期が実行できます。 最終的な RSpecは以下のようになりました。
require "rubygems" gem "rspec" gem "selenium-client" require "selenium/client" require File.expand_path(File.dirname(__FILE__) + "/helper") describe "Todo管理" do attr_reader :selenium_driver alias :page :selenium_driver before(:all) do @verification_errors = [] @selenium_driver = Selenium::Client::Driver.new \ :host => "localhost", :port => 4444, :browser => "*chrome", :url => "http://localhost:3000/", :timeout_in_second => 60 @selenium_driver.start_new_browser_session puts " /test/setup" @selenium_driver.open "/test/setup" @selenium_driver.close_current_browser_session end before(:each) do @selenium_driver.start_new_browser_session end after(:each) do @selenium_driver.close_current_browser_session @verification_errors.should == [] end it "新規作成でTodoが登録できる" do page.open "/todos/" page.click_and_wait_for_page_to_load "link=New Todo" page.type "todo_task", "Seleniumでテスト" page.click_and_wait_for_page_to_load "todo_submit" page.get_text("css=p:nth-child(3)").should == "Task: Seleniumでテスト" end it "一覧で表示をクリックすると詳細が表示される" do page.open "/todos/" page.click "link=Show3" page.wait_for_element_present "css=#detail_area p" page.get_text("css=#detail_area p:nth-child(3)").should == "Task: Seleniumでテスト" end end