過去に 書籍「実践SeleniumWebDriver」のPageObjectパターンをRSpecでテストコードにしてみた。 で「写経」はしてみたのですが、いよいよ実戦で使う時が来たのでこれを思い出す意味で書いていこうと思います。(前任者が残してくれたコード、資料があったのでここまでできるようになりました)
前提
- SeleniumWebDriverとRubyとRSpecはそこそこわかる
- PageObjectデザインパターンもそこそこわかる
- これについては前任者が書いてくれた「Selenium2でつくるテストケースの構成について」 がものすごくわかりやすいです。
- 環境は諸事情によりWindows8.1 (多分Linuxでも同じことができるハズ)ブラウザが立ち上がるのでGUIがあったほうがいいです。
- headlessでもできるみたいです
事前準備
環境設定
-
コマンドプロンプト起動時、自動的に文字コードをUTF-8にして日本語もちゃんと表示できるようにする方法 を参考に、コマンドプロンプトの文字コードをUTF-8に設定しておく。私は Step:2-B の方法を取りました。
-
Windows で Ruby(Railsじゃない環境)でTDDで開発をするための環境構築 2012/8月版 を参考に下記を準備しました
- Ruby 2.2.2 を入れる
- DevKitも入れる
- Bundler 1.10.3を入れる
-
E2E_twディレクトリ作って、その中で作業
bundle install
C:\E2E_tw>bundle init
Writing new Gemfile to C:/E2E_tw/Gemfile
source "https://rubygems.org"
gem "selenium-webdriver"
gem "rspec"
gem "rspec_junit_formatter"
gem "page-object"
page-objectっていうモジュールが優れものっぽいです。使ってみます。
https://github.com/cheezy/page-object/wiki/page-object
bundle install します。
C:\E2E_tw>bundle install --path vendor/bundle
rspec
rspecもinitしておきます。
C:\E2E_tw>rspec --init
create .rspec
create spec/spec_helper.rb
.rspec
の中をみるとこうなっています。
--color
--require spec_helper
--color
出力に色を付けてくれます
--require spec_helper
すべての_spec.rb ファイルに require spec_helper してくれます
requireされるspec_helper
に下記のように書いておきます。
require "rubygems"
require "rspec"
require "page-object"
require "selenium-webdriver"
Dir[File.join(File.dirname(__FILE__), "{pages,operators,support,fixtures}/*.rb")].each{ |f| require f }
RSpec.configure do |config|
# ~デフォルトのまま~
end
Dir[File.join(File.dirname(__FILE__), "{pages,operators,support,fixtures}/*.rb")].each{ |f| require f }
って書くと、一気にrequireできちゃうんですねスゴイ。
何をテストするのか
Twitterで
- 有効なアカウントでログインできること
- 無効なアカウントでログインできないこと
を確認してみます。
コードの設計(構成?)としてはこう
今回はやることが少ないのでoperators
とfixtures
に分ける必要は無いのですが、あえてこの設計で書いてみようと思います。規模が大きくなるとこっちのほうがいいのかなと思いまして。
- features
- テストシナリオ(テストケース?)
- どの画面で、コレして、ソレして、アレしたら、こうなるよね、を書く
- pages
- テスト対象の画面を表現するクラス
- その画面に対して操作できることを書く
- operators
- ページに対する処理をここに書く
- 書き方いろいろあると思うのですが、今回は「pageに対する操作はすべてoperatorを通して行う」で書いてみました
- fixtures
- テストで使うデータセットを書く
C:\E2E_tw
│ .rspec
│
└───spec
│ spec_helper.rb
│
├───features
│ login_spec.rb
│
├───fixtures
│ login_data.rb
│
├───operators
│ login_operator.rb
│
└───pages
login_error_page.rb
login_page.rb
timeline_page.rb
本当はテスト(_spec.rb)から書き始めるのがテストドリブンぽくていいのかもしれませんが、説明上、pageにこんなものがあって、それをspecでこう使ってるよ、っていうほうがわかりやすいと思いまして、pageから説明します。
login_page
↑ログインページを表現するクラスです。
require 'page-object'
class LoginPage
include PageObject
# テストで使う要素を特定
text_field(:id, :id => 'signin-email')
text_field(:pw, :id => 'signin-password')
#button(:submit, :value => 'ログイン') # help me !
button(:submit, :xpath => '//*[@id="front-container"]/div[2]/div[2]/form/table/tbody/tr/td[2]/button')
def initialize(driver)
super(driver)
@driver = driver
@url = "https://twitter.com/"
end
def open
@driver.get(@url)
end
def login_success(id, pw)
self.id = id
self.pw = pw
self.submit
result_page = TimelinePage.new(@driver)
return result_page
end
def login_error(id, pw)
self.id = id
self.pw = pw
self.submit
result_page = LoginErrorPage.new(@driver)
return result_page
end
end
PageObjectの使い方は cheezy/page-object にあります。通常なら要素の特定の時にfind_element
しますが、text_field(:id, :id => 'signin-email')
ように簡単に書けるのが魅力的。
その使い方によれば、上記のログインボタンの要素特定には
#button(:submit, :value => 'ログイン') # help me !
と書けるはずなのですが、動かないので仕方なくxpathで指定しています。(誰か教えて!)
@url
(自分のページを表すurl)をpageに持たせたかったので initialize
メソッドを作ったのですが、PageObjectの使い方のサイトに
Do not create your own initialize method as one already exists and should not be overwritten.
って書いてあったのに super(driver)
をしてなくて動かない時間が数時間ありました。pageobjec
の initialize
の処理は page-object/lib/page-object.rb にあります。(今の私のスキルでは何やってるかよくわかんないけど_| ̄|○)
timeline_page
ログイン成功後の画面を表現するクラスです。
require 'page-object'
class TimelinePage
include PageObject
# 自分のidを表示する要素
span(:my_id, :class => 'u-linkComplex-target')
def initialize(driver)
super(driver)
@driver = driver
end
# id名を取得する
def get_id
return self.my_id
end
end
テストで使う要素の特定と、テストで必要な操作( get_id
)を書きました。
login_error_page
↑ログインに失敗したときの画面を表現するクラスです。
require 'page-object'
class LoginErrorPage
include PageObject
# エラーメッセージが表示される要素
span(:error_msg, :class => 'message-text')
def initialize(driver)
super(driver)
@driver = driver
end
def get_error_msg
return self.error_msg
end
end
テストで使う要素の特定と、テストで必要な操作( get_error_msg
)を書きました。
login_operator
LoginPageに対して操作をするクラスです。
class LoginOperator
def initialize(driver)
@driver = driver
end
def login_success(page, id, pw)
return page.login_success(id,pw)
end
def login_error(page, id, pw)
return page.login_error(id,pw)
end
end
何故にlogin_success
とlogin_error
があるの?という話ですが、PageObject
デザインパターンでは画面遷移を表現するときに、遷移先のpageを返すってのがセオリーだそうです。ですのでログイン成功後に遷移するpage、あるいはログイン失敗後に遷移するpage、を返す必要があります。
login_data
テストで使うデータたちを書いておきます。今回はログインするためのID/PWのセットになります。
module LoginData
module_function
# 登録済ユーザー
def registered_user
data = {
id: "twitter_id",
pw: "twitter_pw",
}
return data
end
# 未登録ユーザー
def unknown_user
data = {
id: "mitourokuyu-za-",
pw: "mitouroku-password",
}
return data
end
end
login_spec
いよいよここまでの材料(?)を使ってテストを書くところです。
describe "LoginPage" do
before do
# ドライバ作って
@driver = Selenium::WebDriver.for :firefox
# ページ作って
@login_page = LoginPage.new(@driver)
# オペレータ作って
@login_op = LoginOperator.new(@driver)
# ページ開く
@login_page.open
end
after do
@driver.quit
end
context '有効アカウント' do
it 'ログインできること' do
# 入力するデータを準備
input_data = LoginData.registered_user
# ログインする(ログイン後のpage=タイムライン画面 が返ってくる)
timeline_page = @login_op.login_success(@login_page, input_data[:id], input_data[:pw])
# タイムライン画面の自分のidが表示されているところにある文字列を取得
my_id = timeline_page.get_id
# 期待値と比較する
expect(my_id).to eq 'twitter_id'
end
end
context '無効アカウント' do
it 'ログインできないこと' do
input_data = LoginData.unknown_user
# ログインしようとする
login_error_page = @login_op.login_error(@login_page, input_data[:id], input_data[:pw])
error_msg = login_error_page.get_error_msg
expect(error_msg).to eq '入力されたユーザー名またはパスワードに誤りがあります。ダブルクリックして再度お試しください。'
end
end
end
(RSpecの書き方とか、もっといいのがあったら教えてほしいです...)
テストを実行する
-fd
オプションを付けて実行してみました。
C:\E2E_tw>rspec -fd
LoginPage
有効アカウント
ログインできること
無効アカウント
ログインできないこと
Finished in 24.52 seconds (files took 0.81775 seconds to load)
2 examples, 0 failures
ブラウザが立ち上がって、twitterにログインできるはずです。
まとめ
というように書いてみたのですがいかがでしょうか。実戦で使ってみて、何かtipsあったらまた投稿しますー。疑問、質問、アドバイスなどありましたら教えて下さい!