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

この記事は[Selenium/Appium Advent Calendar 2016](http://qiita.com/advent-calendar/2016/selenium_and_appium)の10日目の記事です。

## はじめに

[freee株式会社](https://corp.freee.co.jp/)でアプリエンジニアをしている @kompiro と申します。普段は [selenium](http://www.seleniumhq.org/) をガリガリ動かしているエンジニアではないのですが、[SitePrism](https://github.com/natritmeyer/site_prism) というgemを使って PageObjects パターンを実装してみたら、想像以上に捗ったのでご紹介します。

## SitePrism の特徴

SitePrism とは `PageObjectパターンをCapybaraを使って実装するためのDSL` です。 例えば `google.com` のページオブジェクトを SitePrism を使って定義すると下記のようになります。

```ruby
# 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](http://teamcapybara.github.io/capybara/) なので、いざという時 Capybara の書き方に逃げられる


## セットアップ

SitePrism は `selenium-webdriver` と `capybara` にのみ依存します。テスティングフレームワークには依存していません。お好きなフレームワークと組み合わせてください。テストシナリオを読みやすい[Gherkin形式](https://github.com/cucumber/cucumber/wiki/Gherkin)で実装したいなら、[Turnip](https://github.com/jnicklas/turnip)や[Cucumber](https://cucumber.io/)と組み合わせるとよいでしょう。シンプルにRspecと組み合わせる場合は次のように `Gemfile` を定義し、 `bundler` を使ってインストールしましょう。

```Gemfile
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](https://github.com/NoRedInk/rspec-retry) はテスト失敗時に再実行してくれるgem、 [capybara-screenshot](https://github.com/mattheworiordan/capybara-screenshot) はテスト失敗時にスクリーンキャプチャを撮影するgemです。どちらも有用なので、一緒に入れておくのがオススメです。

## PageObjectの定義

SitePrismをつかったページオブジェクトの定義は表現力がある分複雑です。[README](https://github.com/natritmeyer/site_prism#introduction-to-the-page-object-model)に書かれている内容をざっくり翻訳し、まとめました。

### Pageモデル

Pageモデルには2つの宣言を行います。

- URLの宣言
- ページ内の要素の宣言

空のPageモデルを作るには下記のように記述します。

```ruby
class Home < SitePrism::Page
end
```

#### URLの宣言

URLの指定には`#set_url`及び`#set_url_matcher`を宣言します。

```ruby
class Home < SitePrism::Page
  set_url "http://www.google.com"
end
```

Capybaraの設定である`app_host`を宣言しているのであれば、ホスト名までを省略できます。

```ruby
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](https://github.com/sporkmonger/addressable) gem を使っているので、次のように宣言できます。

```ruby
class UserProfile < SitePrism::Page
  set_url "/users{/username}"
end
```

パラメータを宣言した場合、ページを開いたり、検証は次のように行えます。

```ruby
@user_profile = UserProfile.new
@user_profile.load #=> /users
@user_profile.load(username: 'kompiro') #=> loads /users/kompiro

expect(@user_profile).to be_displayed(username: 'kompiro')
```

クエリストリングをパラメータとした場合、ハッシュで値を指定します。

```ruby
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の取得する

```ruby
@home = Home.new
@home.load
@home.current_url # => http://www.google.com
```

#### 現在のページタイトルを取得する

```ruby
@home = Home.new
@home.load
@home.title # => Google
```

### Elementモデル

Pageモデルは単一及び複数のElement(テキストフィールド、ボタン、コンボ等)で構成されます。単一のElementの例は検索フィールドやロゴで、複数のElementの例はメニュー項目やカルーセルの画像です。

#### 単一のElementモデル

単一のElementモデルは簡単に記述できます。

```ruby
class Home < SitePrism::Page
  element :search_field, "input[name='q']"
end
```

ここでは`#element`メソッドは2つの引数を受け取っています。名前としてシンボル、その要素を指定するCSSセレクタを文字列として受け取っています。

##### Elementから値を取得/値を設定する

```ruby
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で文字列を返す
```

##### 要素の存在を検証

```ruby
@home.has_search_field? #=> 存在すればtrue
expect(@home).to have_search_field #=> RSpec MatcherもElementを宣言すれば作成される
```

##### 要素が存在しないことを検証

```ruby
@home.has_no_search_field? #=> 存在しなければtrue
expect(@home).to have_no_search_field #=> RSpec MatcherもElementを宣言すれば作成される
```

##### 要素の作成待ち

ajaxなどでページロードを待つ時に便利

```ruby
@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`で表示、非表示を待つ

```ruby
@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

```ruby
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` で宣言しましょう。

```ruby
class Friends < SitePrism::Page
  elements :names, "ul#names li a"
end
```

##### 複数Elementの扱い方

```ruby
@friends_page = Friends.new
# ...
@friends_page.names #=> [<Capybara::Element>, <Capybara::Element>, <Capybara::Element>]
```

こんな感じで配列で取れるので、検証も次のように行えます。

```ruby
@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モデルを作るには下記のように記述します。

```ruby
class MenuSection < SitePrism::Section
end
```

#### PageモデルにSectionを定義する

PageモデルにSectionを定義するには次のように宣言します。

```ruby
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

```ruby
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テストを行うのであれば是非導入を検討してみてください。