この記事はSelenium/Appium Advent Calendar 2016の10日目の記事です。
はじめに
freee株式会社でアプリエンジニアをしている @kompiro と申します。普段は selenium をガリガリ動かしているエンジニアではないのですが、SitePrism というgemを使って PageObjects パターンを実装してみたら、想像以上に捗ったのでご紹介します。
SitePrism の特徴
SitePrism とは PageObjectパターンをCapybaraを使って実装するためのDSL
です。 例えば google.com
のページオブジェクトを SitePrism を使って定義すると下記のようになります。
# Pageの定義
class Home < SitePrism::Page
set_url 'http://google.com'
element :search_field, "input[name='q']"
element :search_button, "button[name='btnK']"
elements :footer_links, "#footer a"
section :menu, MenuSection, "#gbx3"
def open_search_result
search_button.click
SearchResults.new # 次のページを返す
end
end
class SearchResults < SitePrism::Page
set_url_matcher /google.com\/results\?.*/
section :menu, MenuSection, "#gbx3"
sections :search_results, SearchResultSection, "#results li"
def search_result_links
search_results.map {|sr| sr.title['href']}
end
end
# Sectionの定義:
class MenuSection < SitePrism::Section
element :search, "a.search"
element :images, "a.image-search"
element :maps, "a.map-search"
end
class SearchResultSection < SitePrism::Section
element :title, "a.title"
element :blurb, "span.result-description"
end
このように Page
と Section
の2つを組み合わせられるのが SitePrism
の特徴です。
Page
はページオブジェクトを定義したものです。そのページで行える操作をメソッドに定義し、画面遷移する場合は次ページのオブジェクトを返します。
Section
は複数のページや、一覧ページ等で複数回登場するようなまとまりを定義できます。複数登場しないでも一定のまとまりがあるのであれば、 Section
として宣言しておくと読みやすさが向上します。
Page
と Section
に存在する要素を element
、存在するSectionを section
として宣言します。 それぞれの要素が複数存在するのであれば、 elements
、 sections
と複数形で宣言することもできます。
element
や section
の指定は #footer a
のように、デフォルトではCSSセレクタで行います。もし、CSSセレクタで要素が特定できない場合は、次のようにxpath等でも指定できます。
element :search_field, :xpath, '//*[@id="lst-ib"]'
ただし、:xpath
等の方法で指定するとページの構造により強く依存してしまいます。よりメンテナンス性を高めるには、CSSセレクタで宣言しましょう。
SitePrism
の特徴をまとめると下記の通りです。
- PageObjectの宣言が簡単
- ページ要素の指定も CSSセレクタや xpath など複数の方法を利用できる
-
SitePrism
のベースは Capybara なので、いざという時 Capybara の書き方に逃げられる
セットアップ
SitePrism は selenium-webdriver
と capybara
にのみ依存します。テスティングフレームワークには依存していません。お好きなフレームワークと組み合わせてください。テストシナリオを読みやすいGherkin形式で実装したいなら、TurnipやCucumberと組み合わせるとよいでしょう。シンプルにRspecと組み合わせる場合は次のように Gemfile
を定義し、 bundler
を使ってインストールしましょう。
source 'http://rubygems.org'
gem 'rspec', :require => 'spec'
gem 'rspec-retry'
gem 'selenium-webdriver', '~> 3.0.0'
gem 'capybara'
gem 'capybara-screenshot'
gem 'site_prism'
rspec-retry はテスト失敗時に再実行してくれるgem、 capybara-screenshot はテスト失敗時にスクリーンキャプチャを撮影するgemです。どちらも有用なので、一緒に入れておくのがオススメです。
PageObjectの定義
SitePrismをつかったページオブジェクトの定義は表現力がある分複雑です。READMEに書かれている内容をざっくり翻訳し、まとめました。
Pageモデル
Pageモデルには2つの宣言を行います。
- URLの宣言
- ページ内の要素の宣言
空のPageモデルを作るには下記のように記述します。
class Home < SitePrism::Page
end
URLの宣言
URLの指定には#set_url
及び#set_url_matcher
を宣言します。
class Home < SitePrism::Page
set_url "http://www.google.com"
end
Capybaraの設定であるapp_host
を宣言しているのであれば、ホスト名までを省略できます。
class Home < SitePrism::Page
set_url "/home.htm"
end
Pageモデルの URLの宣言
はオプションです。ページ読込や検証で利用しないのであれば宣言する必要はありません。
ページを開く
次のようにすると、宣言したURLを開きます。
@home = Home.new
@home.load
ページ遷移後、意図したURLを開いているか検証する
expect(Home.new).to be_displayed
URLにパラメータがある場合
SitePrismのURLには Addressable gem を使っているので、次のように宣言できます。
class UserProfile < SitePrism::Page
set_url "/users{/username}"
end
パラメータを宣言した場合、ページを開いたり、検証は次のように行えます。
@user_profile = UserProfile.new
@user_profile.load #=> /users
@user_profile.load(username: 'kompiro') #=> loads /users/kompiro
expect(@user_profile).to be_displayed(username: 'kompiro')
クエリストリングをパラメータとした場合、ハッシュで値を指定します。
class Search < SitePrism::Page
set_url "/search{?query*}"
end
@search = Search.new
@search.load(query: 'simple') #=> loads /search?query=simple
@search.load(query: {color: 'red', text: 'blue'}) #=> loads /search?color=red&text=blue
expect(@ search.url_matches['query']['color']).to eq "red"
現在のURLの取得する
@home = Home.new
@home.load
@home.current_url # => http://www.google.com
現在のページタイトルを取得する
@home = Home.new
@home.load
@home.title # => Google
Elementモデル
Pageモデルは単一及び複数のElement(テキストフィールド、ボタン、コンボ等)で構成されます。単一のElementの例は検索フィールドやロゴで、複数のElementの例はメニュー項目やカルーセルの画像です。
単一のElementモデル
単一のElementモデルは簡単に記述できます。
class Home < SitePrism::Page
element :search_field, "input[name='q']"
end
ここでは#element
メソッドは2つの引数を受け取っています。名前としてシンボル、その要素を指定するCSSセレクタを文字列として受け取っています。
Elementから値を取得/値を設定する
class Home < SitePrism::Page
set_url "http://www.google.com"
element :search_field, "input[name='q']"
end
@home = Home.new
@home.load
@home.search_field #=> 存在していればCapybara::Elementが返ってくる
@home.search_field.set "検索文字列" #=> CapybaraのAPIをそのまま使える
@home.search_field.text #=> #textはCapybaraのAPIで文字列を返す
要素の存在を検証
@home.has_search_field? #=> 存在すればtrue
expect(@home).to have_search_field #=> RSpec MatcherもElementを宣言すれば作成される
要素が存在しないことを検証
@home.has_no_search_field? #=> 存在しなければtrue
expect(@home).to have_no_search_field #=> RSpec MatcherもElementを宣言すれば作成される
要素の作成待ち
ajaxなどでページロードを待つ時に便利
@home = Home.new
@home.load
@home.wait_for_search_field # Capybara.default_wait_max_time だけまつ
@home.wait_for_search_field(10) # 10秒まつ
要素の表示/非表示待ち
#wait_until_<element_name>_visible
及び#wait_until_<element_name>_invisible
で表示、非表示を待つ
@home.wait_until_search_field_visible #=> Capybara.default_wait_max_time 表示待ち
@home.wait_until_search_field_visible(10) #=> 10秒表示待ち
@home.wait_until_search_field_invisible #=> Capybara.default_wait_max_time 非表示待ち
@home.wait_until_search_field_invisible(10) #=> 10秒非表示待ち
CSS Selectors vs. XPath Expressions
class Home < SitePrism::Page
# CSS Selector:
element :first_name, "div#signup input[name='first-name']"
# XPath expression:
element :first_name, :xpath, "//div[@id='signup']//input[@name='first-name']"
end
特徴のところでも書きましたが、XPathで記述するとDOM構造により強く依存してしまうのでCSS Selectorで書くほうがオススメです。
複数のElement
複数のElementが存在する場合は #elements
で宣言しましょう。
class Friends < SitePrism::Page
elements :names, "ul#names li a"
end
複数Elementの扱い方
@friends_page = Friends.new
# ...
@friends_page.names #=> [<Capybara::Element>, <Capybara::Element>, <Capybara::Element>]
こんな感じで配列で取れるので、検証も次のように行えます。
@friends_page.names.each {|name| puts name.text}
expect(@friends_page.names.map {|name| name.text}.to eq ["Alice", "Bob", "Fred"]
expect(@friends_page.names.size).to eq 3
expect(@friends_page).to have(3).names
#elements
も #element
と同様、 #has_<elements name>?
、 #wait_for_<elements name>
等が自動で追加されます。
宣言したElementが全部あるか検証するには?
#all_there?
というメソッドと、 be_all_there
というMatcherがページオブジェクトに定義されているので、それを使って検証します。
Sectionモデル
Sectionモデルはページ内のまとまりごとに宣言します。空のSectionモデルを作るには下記のように記述します。
class MenuSection < SitePrism::Section
end
PageモデルにSectionを定義する
PageモデルにSectionを定義するには次のように宣言します。
class Home < SitePrism::Page
section :menu, MenuSection, "#gbx3"
end
#element
と#section
の違いは、第2引数にSectionモデルのクラスを指定することだけです。1ページ内に同じSectionが複数定義したい場合は#sections
と宣言すれば良く、XPathで指定したいのであれば、第3引数を:xpath、第4引数にxpath表現を文字列で指定します。
SectionモデルはElementモデルのようなPage内の要素としての機能とPageモデルのコンテナ的な機能両方の要素を兼ねたモデルです。そのため、Sectionの扱い方はElementのアクセス方法と同じく、またSection内のElementやSectionの宣言はPageと同じなので割愛します。
無名Section
class Home < SitePrism::Page
section :menu, '.menu' do
element :title, '.title'
elements :items, 'a'
end
end
Sectionはクラスとして宣言しなくても、ブロックとして定義できます。
まとめ
RubyでPageObjectsパターンを実装するためのgem、SitePrismを紹介しました。僕はかれこれ4回ほどGUIテストの自動化プロジェクトに関わったことがありますが、SitePrismを利用した時ほどわかりやすく実装ができたプロジェクトはありませんでした。もしRubyを使ってE2Eテストを行うのであれば是非導入を検討してみてください。