広告技術部のサンドバーグと星です。 普段の業務は、主に広告の管理システムの開発をしています。管理画面はRuby on Railsで作られており、今回は煩雑になりがちなE2Eのテストをきれいに書けたので、それについて話します。
背景
Gunosyの広告システムは4年以上前にリリースされ、これまで多くの機能が追加されてきました。 配信システムは一度リプレースされましたが、私達が運用している管理画面に関してはリプレース などはされず、現在も拡張され続けています。長く運用されているシステムのため 開発するメンバーの入れ替わりもあり、もちろん思想やコードスタイルも変わってきたため、 バグが発生しやすい環境になってしまっています。
ただ、外部のお客様も使う機能も含まれるため、バグが無いことを担保する必要があり、 テストがより重要になってきます。
また、複雑なデータ構造と画面操作があるため、単体テストでバグが無いことを担保するのは難しい。 そこで、E2Eのテストを充実させ、期待する入力と出力が正しいことを保証しています。
E2EのテストはRspecとCapybaraを使っています。
Gunosyの複雑なテスト
context 'when logged in as operator', js: true do it 'editable approval_status, provisional_approval_status, refusal_reason & comment' do expect(page.all('tbody#records > tr > td > a.editable')[0].text).to eq 'waiting_for_approval' page.all('tbody#records > tr > td > a.editable')[0].trigger('click') expect(page.find('div.editable-input select').find(:xpath, 'option[1]').text).to eq 'waiting_for_approval' expect(page.find('div.editable-input select').find(:xpath, 'option[2]').text).to eq 'accepted' expect(page.find('div.editable-input select').find(:xpath, 'option[3]').text).to eq 'refused' page.find('div.editable-input select').select 'accepted' page.find('div.editable-buttons button.editable-submit').click expect(page.all('tbody#records > tr > td > a.editable')[1].text).to eq '未対応' page.all('tbody#records > tr > td > a.editable')[1].trigger('click') expect(page.find('div.editable-input select').find(:xpath, 'option[1]').text).to eq 'OK' expect(page.find('div.editable-input select').find(:xpath, 'option[2]').text).to eq '不明' expect(page.find('div.editable-input select').find(:xpath, 'option[3]').text).to eq 'NG' expect(page.find('div.editable-input select').find(:xpath, 'option[4]').text).to eq '未対応' page.find('div.editable-input select').select 'OK' page.find('div.editable-buttons button.editable-submit').click page.all('tbody#records > tr > td > a.editable')[3].trigger('click') page.find('div.editable-input textarea').set 'テストコメント' page.find('div.editable-buttons button.editable-submit').click expect(page.all('a.creative_editable').first.text).to eq '承認済み' expect(page.all('a.creative_editable')[1].text).to eq 'OK' expect(page.all('a.creative_editable')[2].text).to eq 'その他' expect(page.all('a.creative_editable')[3].text).to eq 'テストコメント' end end
上記のようにE2Eのテストをしようとすると、
- Elementを検索・操作する際に全体ページから一意の要素を逐一検索する必要があります
- ElementはHTMLの階層構造で表現されているため、何なのかがわかりづらいです
- 同じ要素を繰り返して使う際に独自クラスなどを定義しない限り、都度ページ要素を検索する必要があります
- DOMに変更があった場合、各Elementの検索を変更する必要が出て来るかもしれません
また、要素アクセスのためのコードは冗長で見通しが非常に悪いです。
こういったE2Eテストの問題を新規プロジェクト開始のタイミングできれいに保てるような仕組みを 導入してみました。
SitePrism *1
SitePrismとは...
A Page Object Model DSL for Capybara
Capybaraのテストで利用するページを Page Object
として利用することができます。
Page Object
とは、一つのHTMLページを一つのオブジェクトとしてとらえるデザインパターンです。
以下は README.me
から参照した、クラスの定義とその使用例です。
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の要素を取得できます @home.search_field.set 'hoge' #=> セレクタはCapybaraのElementを返すため、Capybaraのメソッドを利用できます @home.search_field.text #=> 'hoge'
Gunosyでの使用例
HTMLの構造が複雑であるため、 SitePrismのセクション機能を使って、ひとかたまりの要素を抽象化しました。
ページクラス
class Index < SitePrism::Page class SearchForm < SitePrism::Section element :search_button, 'td:nth-child(2) button' element :user_name, '.search-detail input#user_name' end class TableRow < SitePrism::Section element :user_id, 'td div div:nth-child(2) .text-small' element :user_name, 'td div div:nth-child(2) .text-default' end set_url '/users' element :title, '.page-title' section :search_form, SearchForm, '.search-bar' sections :table_rows, TableRow, '.table tbody tr' def table_rows_of(index) table_rows[index] end end
このユーザ一覧ページの例では、ユーザを表示するテーブルと、ユーザを検索するフォームに分けることができます。
これらの機能をsection
として定義することで、参照しやすい単位に抽象化することができます。
E2Eテスト
let(:index) { Pages::User::Index.new } before { index.load } context 'when displaying tables' do it 'should display correct sum values' do expect(index.title).to have_content(I18n.t('term.user_index')) expect(index.table_rows_of(0).user_name.text).to have_content(user.name) expect(index.table_rows_of(0).user_id.text).to have_content("ID #{user.id}") end context 'when user id is specified', js: true do before do index.search_form.user_name.set user.name index.search_form.search_button.click end it 'should display 1 row' do expect(index).to have_table_rows count: 1 expect(index.table_rows_of(0).user_name.text).to have_content(user.name) end end end
ページクラスのインスタンスを使って、CapybaraのElementを取り出せるので、そのまま Capybaraのメソッドが利用できます。 これによって、複雑な構造でも要素を扱いやすくなります。
最後に
新規のプロジェクトから導入したSitePrismでより良いテストコードを書くことができました。 今回は新規のプロジェクトでの取り組みでしたが、既存のシステムでもSitePrism を使い、より質の高いテストコードにしていきたいと思います。
私の所属する広告技術部ではRails管理画面に限らず広く一緒にサーバーサイド開発してくれる仲間を募集中です。ご興味あるエンジニアの方は下記から応募してみてください!
*1:masterブランチではテストが落ちているので、こちらのPRで運用していますhttps://github.com/natritmeyer/site_prism/pull/186