この記事はSmartHR Advent Calendar 2019 2日目の記事です。
SmartHRではRuby on Railsを広く採用しています。アプリケーションを長期的にメンテナンスしていくためにテストは欠かせません。特にReact.jsなどを用いた複雑なUIにおいては、単なるAPIのテストやモデルのテストだけではなく「実際にブラウザを操作して、ユーザーが期待する結果を得られるかどうか」をテストすることが重要です。
Rubyではこのようなブラウザを操作するテストを書くために、Capybaraという便利なフレームワークがあり、比較的簡単にテストを書き始めることができます。ただ、この手のテストは保守が大変であったり、手間の大きさからテストが追加されなくなったり、ということがよくあります。本記事では、私がこれまでの経験から学んだ、壊れにくいテストを書くためのTipsを紹介します。
なお、特に説明のない限り、ここではCapybara + RSpec + Selenium + Chrome (Headless)の環境を想定しています。
sleep
しない
出オチっぽいですが、非常に重要です。非同期なリクエストの結果を待っているときや、時間差でレンダリングされる画面など「いい感じに少し待って」と言いたくなる状況は確かにあります。
そういった場合に、単にsleep 5
などとしてしまうと
- テストを実行する環境によって適切な待ち時間が異なるため、落ちたり落ちなかったりするテストが生まれやすい
- テストが落ちたときに、適当に待ち時間を伸ばされることが多く、テストの実行時間が伸びがち
などの問題があります。このような場合では、何を待っているかを短いスパンで定期的にタイムアウトまで待つようなヘルパーメソッドを定義して、それを利用するようにしましょう。
it "何かのアクションの結果、Successメッセージが帰ってくる" do
click_button "何かのアクション"
finally do
expect(page).to have_content "Success!"
end
end
def finally(timeout: Capybara.default_max_wait_time)
start = Time.now
begin
yield
return
rescue RSpec::Expectations::ExpectationNotMetError, Capybara::ElementNotFound
raise if Time.now > start + timeout
sleep 0.1
retry
end
end
XPathやclassに依存しない
Capybaraではhave_button
やfill_in
など基本的なHTMLの要素に対するヘルパーが定義されているため、素直な画面に対しては比較的読みやすいテストを書くことができます。
しかし、現実には画面が複雑な構造になっていることが多く、これらのヘルパーだけでは力不足であることはよくあります。そういったときに、XPathやclassに依存したテストを書いてしまうこともきっとよくあるでしょう。
find('form active-form button').click
expect(page).to have_xpath '//*[@id="form"]/div[2]'
しかし、これでは後からテストだけ見たときに、何をテストしているのかわからなくなってしまいます。例えば、あなたが画面を大幅に弄った後に、「よーし、テスト直すかー」とこのテストを見たら... きっとこのテストごと消えてしまうことになるでしょう。
このような悲劇を産まないためにも、テストは後から読めるように、XPathやclassに依存しないことをおすすめしています。とはいえ、素直にヘルパーが利用できない画面というのは当然ありますから、話はそんな簡単ではありません。こういった場合にはデータ属性を利用し、さらにそれを指定するヘルパーメソッドを生やすと良い感じになります。
click_form_button
expect_to_have_error_message
def click_form_button
find(spec_selector('active-form-button')).click
end
def expect_to_have_error_message
expect(page).to have spec_selector('active-form-error-message')
end
def spec_selector(name)
"[data-spec='#{name}']"
end
データ属性は他の用途に利用されることがなく、自由に目印をつけられるので、テストを見たときに何を指しているかわかりやすく、HTML側の編集時にも目を引く良い方法です。適切な単位でデータ属性を割り当てていれば「なぜかわからないけどdivをひとつズラしたらテストが通らなくなった」といった問題も起きにくくなるでしょう。
within
を活用する
この記事をテストすると仮定して、「本文の書き出しに"Capybara"というリンクが含まれていること」をテストするとします。
expect(page).to have_link "Capybara", href: "https://github.com/teamcapybara/capybara"
これでもテストは通りますが、これでは「本文の書き出しの中に」という重要な条件が抜けてしまっています。例えば、末尾の参考文献に"Capybara"を含むリンクを追加した途端、本当にテストしたかった書き出しのリンクが消えても、テストが通る状態になってしまいます。
他にも「保存」ボタンをクリックしたい状況があるとして、同じ画面中にいくつも「保存」ボタンがあると、単にclick_button "保存"
では、Ambiguous matchを引き起こしてしまいます。match: :first
やall
してアクセスする方法もありますが、あまり良い方法ではありませんよね。
click_button "保存", match: :first # firstって何?
all('button', text: "保存")[1].click # うーん...
こういった場合では、within
によるスコープの絞り込みが役に立ちます。
it "書き出しにリンクが含まれる" do
within_introduction do
expect(page).to have_link "Capybara", href: "https://github.com/teamcapybara/capybara"
end
end
it "ヘッダーの保存ボタンをクリック" do
within_header do
click_button "保存"
end
end
def within_introduction
within(spec_selector("introduction")) { yield }
end
def within_header
within(spec_selector("header")) { yield }
end
データ属性を使ったヘルパーメソッドの定義と合わせると、随分と読みやすく感じるはずです。
ユーザーの目に見えないもの(気にしないもの)をテストしない
これは書き方というか、心構えの問題だと思うのですが、基本的にデータベースの中身だったり、DOMの構造など「ユーザーが意識しないもの」はテストするべきではない、と考えています。例えば、こんなテストです。
visit edit_user_path(user)
fill_in "名前", with: "新しい名前"
click_button "保存"
expect(user).to have_attribute(name: "新しい名前")
もちろん、こういったテストを書かざるを得ない状況というのもあると思うのですが、可能な限りユーザーの体験をテストしたいので、ユーザーが知ることができないデータベースの値をテストするのは望ましくないでしょう。実際にユーザーが更新済みの値を見ることができる画面でテストするべきです。
visit edit_user_path(user)
fill_in "名前", with: "新しい名前"
click_button "保存"
expect(page).to have_current_path user_path(user)
expect(page).to have_content "新しい名前"
ボタンクリックなどの操作も同様です。ユーザーは「divタグの3番目の中のボタンをクリックするぞ!」とクリックすることはありませんよね。within
などと組み合わせて「新着メニューの中にあるボタンをクリックする」というように表現すると、後から見た時に読みやすくなります。
# Bad
all('button', text: "詳細")[2].click
# Good
within_new_menu do
click_button "詳細"
end
ブラウザのサイズを大きくする
当たり前のことだからしれませんが、あまり言及されている印象がないので書いておきます。ブラウザのサイズは大きければ大きいほどいいです。
Capybara.register_driver(:chrome_headless) do |app|
options = Selenium::WebDriver::Chrome::Options.new(args: [
"window-size=3000,3000",
"headless",
"disable-gpu",
])
Capybara::Selenium::Driver.new(app, browser: chrome, options: options)
end
ブラウザのサイズが小さい場合、別の要素が被ってくることによって、テストが落ちるなどの問題が起きることがあります。もちろん、ブラウザサイズが小さい画面でテストをしたい状況もあるので、必ずしもこの手が使えるわけではないのですが、特に理由がないならば、ある程度大きく設定しておくことをおすすめします。
簡単にブラウザを起動できる環境を用意する
CIでテストを回すことを考えると、ヘッドレスモードでChromeを動かすことになると思いますが、開発中やテストを書いている段階では、実際にブラウザが立ち上がって操作しているところを見れる方がテンションもあがりますし、問題を特定しやすくなります。
個人的には、環境変数でdriverを簡単に切り替えられるようにしておくと、さっとブラウザを起動してテストが落ちた原因を探ることができるので便利です。
Capybara.configure do
config.default_driver = ENV['FOREGROUND'] ? :chrome : :chrome_headless
end
おわりに
ブラウザを操作するテストはコストが高く、メンテが難しい、という意見をよく聞きますが、個人的にはコツを抑えて書けば、もっとうまくできるのではないかと思っています。
SmartHRもまだ十分と言える状況ではありませんが、QAチームとも協力しながら、うまくテストを増やしていきたいところです。