14
12

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.

SmartHRAdvent Calendar 2019

Day 2

Capybaraで壊れにくいテストを書くために気を付けていること

Posted at

この記事は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_buttonfill_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: :firstallしてアクセスする方法もありますが、あまり良い方法ではありませんよね。

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チームとも協力しながら、うまくテストを増やしていきたいところです。

14
12
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
14
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?