56
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Selenium/AppiumAdvent Calendar 2016

Day 10

RubyでPageObjectsパターンを実装できる SitePrism のご紹介

Last updated at Posted at 2016-12-10

この記事は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

このように PageSection の2つを組み合わせられるのが SitePrism の特徴です。

Page はページオブジェクトを定義したものです。そのページで行える操作をメソッドに定義し、画面遷移する場合は次ページのオブジェクトを返します。

Section は複数のページや、一覧ページ等で複数回登場するようなまとまりを定義できます。複数登場しないでも一定のまとまりがあるのであれば、 Section として宣言しておくと読みやすさが向上します。

PageSection に存在する要素を element 、存在するSectionを section として宣言します。 それぞれの要素が複数存在するのであれば、 elementssections と複数形で宣言することもできます。

elementsection の指定は #footer a のように、デフォルトではCSSセレクタで行います。もし、CSSセレクタで要素が特定できない場合は、次のようにxpath等でも指定できます。

element :search_field, :xpath, '//*[@id="lst-ib"]'

ただし、:xpath等の方法で指定するとページの構造により強く依存してしまいます。よりメンテナンス性を高めるには、CSSセレクタで宣言しましょう。

SitePrism の特徴をまとめると下記の通りです。

  • PageObjectの宣言が簡単
  • ページ要素の指定も CSSセレクタや xpath など複数の方法を利用できる
  • SitePrism のベースは Capybara なので、いざという時 Capybara の書き方に逃げられる

セットアップ

SitePrism は selenium-webdrivercapybara にのみ依存します。テスティングフレームワークには依存していません。お好きなフレームワークと組み合わせてください。テストシナリオを読みやすいGherkin形式で実装したいなら、TurnipCucumberと組み合わせるとよいでしょう。シンプルに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テストを行うのであれば是非導入を検討してみてください。

56
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
56
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?