Ruby on Railsアプリ以外でも Selenium RC を使えば RSpec でテストが書ける!


RSpec のテストを記述するDSLとしては素晴らしさは、 Selenium RC と組み合わせる事で Ruby on Railsアプリ以外でも使えます。

SeleniumJavascriptを使いWebアプリのテストを行うツールとして有名ですが、Selenium RC を使うと RSpecで書いたテストを Webブラウザー上で走らせる事が出来ます。

http://seleniumhq.org/images/big-logo.png

準備

Seleniumのページから Selenium IDESelenium RC (Remote Control) をダウンロードします。
Selenium IDEFirefox上で動きますので 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による画面操作が終わったか自動的には判定できないのです。

ここ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 を実行するには

  1. selenium-client のインストール sudo gem install selenium-client
  2. Selenium RC を別ターミナルで実行 java -jar selenium-server-standalone-2.0b3.jar

の準備を行い、RSpec形式で保存したファイルを RSpecで実行します。ブラウザーが立ち上がりテストが実行されます。

% spec -c -f s new_spec.rb 

Selenium IDEが作る RSpecRSpec 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