Capybara
selenium-webdriver

CapybaraとSitePrismを使ってみる

元になるコード:

元になるコードの量が増えてきたのでGitで公開するべきなのだろうか…?

E2Eテストコードの可読性やメンテナンス性を考慮すると、ページの各要素や動作をオブジェクト化するページオブジェクトデザインパターンを用いるのが良いとされています。

Ruby+Selenium WebDriver+Capybaraを使用する場合、ページオブジェクトモデルDSLである「SitePrism」のgemを利用するのが良さそうです。

SitePrism: https://github.com/natritmeyer/site_prism
SitePrism(RubyGems): https://rubygems.org/gems/site_prism

早速、SitePrismとサンプルコードを動かしてみましょう。

SitePrism gemのインストール

Gemfileに以下のgemを追加します。

Gemfile
# https://rubygems.org/gems/site_prism
gem 'site_prism', '~> 2.11'

次にbundle install(bundle install --path vendor/bundle)を実行します。

$ bundle install
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies...
Using public_suffix 3.0.2
Using addressable 2.5.2
Using bundler 1.16.1
Using mini_mime 1.0.0
Using mini_portile2 2.3.0
Using nokogiri 1.8.2
Using rack 2.0.4
Using rack-test 0.8.3
Using xpath 3.0.0
Using capybara 2.18.0
Using ffi 1.9.23
Using childprocess 0.9.0
Using diff-lcs 1.3
Using rspec-support 3.7.1
Using rspec-core 3.7.1
Using rspec-expectations 3.7.0
Using rspec-mocks 3.7.0
Using rspec 3.7.0
Using rubyzip 1.2.1
Using selenium-webdriver 3.11.0
Fetching site_prism 2.11
Installing site_prism 2.11
Bundle complete! 4 Gemfile dependencies, 21 gems now installed.
Bundled gems are installed into `./vendor/bundle`

capybara.rb側のrequireはspec_helper.rbのみにします。

spec/helpers/capybara.rb
require_relative '../spec_helper.rb'

spec_helper.rb側のrequireは以下のように変更します。

spec/spec_helper.rb
require 'capybara/rspec'
require 'selenium-webdriver'
require 'logger'
require_relative './helpers/navigation_listener.rb'
require_relative './helpers/capybara.rb'

ページオブジェクトファイルの作成

ページオブジェクトを記載するファイルを作成します。

mkdir spec/pages
touch spec/pages/google.rb

以下のようにページオブジェクトファイルを作成します。基本的にSitePrismリポジトリのREADME.mdに少し手を入れたものになります。

なお、classの読み込み順でエラーになるため、class Homeclass SearchResults内で参照している、class MenuSectionclass SearchResultSectionを先に持ってきています。

spec/pages/google.rb
require 'site_prism'

module Google

  # トップページ最上部のメニュー
  class MenuSection < SitePrism::Section
    element :gmail, :xpath, '//a[text()="Gmail"]'
    element :images, :xpath, '//a[text()="画像"]'
    element :google_apps, :xpath, '//a[@title="Google アプリ"]'
    element :login_button, :xpath, '//a[text()="ログイン"]' # ログアウト時
  end

  # トップページ最下部のフッター
  class FooterSection < SitePrism::Section
    element :ads, :xpath, '//a[text()="広告"]'
    element :services, :xpath, '//a[text()="ビジネス"]'
    element :about, :xpath, '//a[text()="Googleについて"]'
    element :privacy, :xpath, '//a[text()="プライバシー"]'
    element :policies, :xpath, '//a[text()="規約"]'
    element :preferences, :xpath, '//a[text()="設定"]'
  end

  class SearchResultSection < SitePrism::Section
    element :title, 'a'
  end

  # トップページの各オブジェクトと機能
  class Home < SitePrism::Page
    set_url 'https://www.google.co.jp/'
    set_url_matcher %r{www.google.co.jp/?}

    section :menu, MenuSection, '#gb'

    element :search_form, :xpath, '//form[@id="tsf"]'
    element :search_field, 'input[name="q"]'
    element :search_button, "input[name='btnK']"

    section :footer, FooterSection, '#footer'
  end

  # 検索結果ページの各オブジェクトと機能
  class SearchResults < SitePrism::Page
    set_url_matcher %r{www.google.co.jp/search?.*} # リンクなどから遷移してきた時にチェックされる

    sections :search_results, SearchResultSection, :xpath, '//h3[@class="r"]'

    # 検索条件に応じた検索結果の各リンクのテキストを取得する
    def search_result_links
      search_results.map { |sr| sr.title.text }
    end
  end

end # Google

スペックファイルの作成

ページオブジェクトファイルを利用するスペックファイルを作成します。

touch spec/features/example_site_prism_spec.rb

作成したexample_site_prism_spec.rbをコーディングしていきます。
テストの流れ的には以下のようになっています。

  1. Googleトップページのヘッダー部分の検証
  2. Googleトップページのフッター部分の検証
  3. Googleトップページのフォーム部分の検証
  4. 任意のワードで検索して、検索結果ページに目的のページが含まれていることを検証
spec/features/example_site_prism_spec.rb
require 'spec_helper.rb'
require_relative '../pages/google.rb'

describe 'Site_Prism Example' do

  # スペック実行時に最初に実行される処理
  before :all do
    @home = Google::Home.new
  end

  # テスト(it)が実行される前に毎回実行される処理
  before :each do
    @home.load
  end

  it 'トップページ最上部に各メニューが表示されていること' do
    @home.wait_for_menu
    expect(@home).to have_menu
    expect(@home.menu).to have_gmail
    expect(@home.menu).to have_images
    expect(@home.menu).to have_google_apps
    expect(@home.menu).to have_login_button
  end

  it 'トップページのフッターに各メニューが表示されていること' do
    @home.wait_for_footer
    expect(@home).to have_footer
    expect(@home.footer).to have_ads
    expect(@home.footer).to have_services
    expect(@home.footer).to have_about
    expect(@home.footer).to have_privacy
    expect(@home.footer).to have_policies
    expect(@home.footer).to have_preferences
  end

  it 'トップページに検索フォームの要素が存在すること' do
    @home.wait_for_search_form
    expect(@home).to have_search_form
    expect(@home).to have_search_field
    expect(@home).to have_search_button
  end

  it '検索結果の1ページ目に検索対象のページタイトルが表示されること' do
    @home.search_field.set 'Sausages'
    @home.search_field.native.send_keys(:tab) # Suggestionsメニューを閉じる
    @home.search_button.click
    @results_page = Google::SearchResults.new
    expect(@results_page).to be_displayed
    @results_page.wait_for_search_results
    expect(@results_page).to have_search_results count: 10
    expect(@results_page.search_result_links).to include 'Sausage - Wikipedia'
  end

end

スペックの実行

ページオブジェクトファイルとスペックファイルが完成したら実行してみます。

$ bundle exec rspec spec/features/example_site_prism_spec.rb

Site_Prism Example
    2018-03-25 01:38:58 +0900: 次の要素が見つかりました: type: css selector, value: #gb
    2018-03-25 01:38:58 +0900: 次の要素が見つかりました: type: css selector, value: #gb
    2018-03-25 01:38:58 +0900: 次の要素が見つかりました: type: css selector, value: #gb
    2018-03-25 01:38:58 +0900: 次の要素が見つかりました: type: xpath, value: //a[text()="Gmail"]
    2018-03-25 01:38:58 +0900: 次の要素が見つかりました: type: css selector, value: #gb
    2018-03-25 01:38:58 +0900: 次の要素が見つかりました: type: xpath, value: //a[text()="画像"]
    2018-03-25 01:38:58 +0900: 次の要素が見つかりました: type: css selector, value: #gb
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: xpath, value: //a[@title="Google アプリ"]
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: css selector, value: #gb
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: xpath, value: //a[text()="ログイン"]
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: xpath, value: /html/body/*
  トップページ最上部に各メニューが表示されていること
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: css selector, value: #footer
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: css selector, value: #footer
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: css selector, value: #footer
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: xpath, value: //a[text()="広告"]
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: css selector, value: #footer
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: xpath, value: //a[text()="ビジネス"]
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: css selector, value: #footer
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: xpath, value: //a[text()="Googleについて"]
    2018-03-25 01:38:59 +0900: 次の要素が見つかりました: type: css selector, value: #footer
    2018-03-25 01:39:00 +0900: 次の要素が見つかりました: type: xpath, value: //a[text()="プライバシー"]
    2018-03-25 01:39:00 +0900: 次の要素が見つかりました: type: css selector, value: #footer
    2018-03-25 01:39:00 +0900: 次の要素が見つかりました: type: xpath, value: //a[text()="規約"]
    2018-03-25 01:39:00 +0900: 次の要素が見つかりました: type: css selector, value: #footer
    2018-03-25 01:39:00 +0900: 次の要素が見つかりました: type: xpath, value: //a[text()="設定"]
    2018-03-25 01:39:00 +0900: 次の要素が見つかりました: type: xpath, value: /html/body/*
  トップページのフッターに各メニューが表示されていること
    2018-03-25 01:39:00 +0900: 次の要素が見つかりました: type: xpath, value: //form[@id="tsf"]
    2018-03-25 01:39:00 +0900: 次の要素が見つかりました: type: xpath, value: //form[@id="tsf"]
    2018-03-25 01:39:00 +0900: 次の要素が見つかりました: type: css selector, value: input[name="q"]
    2018-03-25 01:39:00 +0900: 次の要素が見つかりました: type: css selector, value: input[name='btnK']
    2018-03-25 01:39:01 +0900: 次の要素が見つかりました: type: xpath, value: /html/body/*
  トップページに検索フォームの要素が存在すること
    2018-03-25 01:39:01 +0900: 次の要素が見つかりました: type: css selector, value: input[name="q"]
    2018-03-25 01:39:01 +0900: 次のスクリプトを実行しようとしています: arguments[0].value = ''
    2018-03-25 01:39:01 +0900: スクリプトを実行しました
    2018-03-25 01:39:01 +0900: 次の要素に関する値を変更しようとしています: tag_name: input class: gsfi lst-d-f
    2018-03-25 01:39:01 +0900: 次の要素に関する値を変更しました: tag_name: input class: gsfi lst-d-f
    2018-03-25 01:39:01 +0900: 次の要素が見つかりました: type: css selector, value: input[name="q"]
    2018-03-25 01:39:01 +0900: 次の要素に関する値を変更しようとしています: tag_name: input class: gsfi lst-d-f
    2018-03-25 01:39:02 +0900: 次の要素に関する値を変更しました: tag_name: input class: gsfi
    2018-03-25 01:39:02 +0900: 次の要素が見つかりました: type: css selector, value: input[name='btnK']
    2018-03-25 01:39:03 +0900: 次の要素をクリックしました: input
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: xpath, value: //h3[@class="r"]
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: xpath, value: //h3[@class="r"]
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: xpath, value: //h3[@class="r"]
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: css selector, value: a
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: css selector, value: a
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: css selector, value: a
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: css selector, value: a
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: css selector, value: a
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: css selector, value: a
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: css selector, value: a
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: css selector, value: a
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: css selector, value: a
    2018-03-25 01:39:03 +0900: 次の要素が見つかりました: type: css selector, value: a
    2018-03-25 01:39:04 +0900: 次の要素が見つかりました: type: xpath, value: /html/body/*
  検索結果の1ページ目に検索対象のページタイトルが表示されること

Finished in 7.66 seconds (files took 0.50292 seconds to load)
4 examples, 0 failures

    2018-03-25 01:39:04 +0900: ブラウザーを終了しました

まとめ

Selenium WebDriver、RSpec、Capybara、SitePrismそれぞれの記法を覚える必要があるので学習コストはそれなりにかかりそうです。SitePrismに関しては一定のルール(記法)に乗っ取ってページオブジェクトファイルを作成するのでとっかかりには良いかも知れません。

しかし、作成したコードは必ずメンテナンスが必要になり、それなりにコストが必要になるため、出来る限り誰でも読みやすくメンテナンスのしやすいコードが書ける方法を模索する必要があります。SitePrismはその一助となるかも知れません。