cuzic です。
これは、 Ruby on Rails Advent Calendar 2019 の 14日目の記事です。
はじめに
ENECHANGE では Ruby on Rails でアプリケーションを構築しており、 Circle CI で自動的にテストを実行しています。
ENECHANGE では以前から Circle CI のテストがしばしば失敗するという現象に悩まされていました。たいていの場合、 Rebuild をクリックしたら成功します。が、リリースが遅くなってしまいます。ときには残業をして対応することもあり、エンジニアの負担となっていました。
こうした状況は、健全な状況ではあるとは言えません。今回、一念発起して、たまに落ちるテストがなぜ落ちるのか調査し、それを改善するように対応しました。
これまでの取り組み
ENECHANGE ではこのようなたまに落ちるテストについてずっと放置していたわけではありません。
過去も、下記の記事や
下記の記事で tech.enechange.co.jp
取り組んでいたように、たまに落ちるテストにはずっと取り組んできました。
しかしながら、それだけではすべて解決するのは難しいという状況でした。
状況の調査
たまに落ちるテストがなぜたまに落ちるのかの原因調査を実施しました。 これはたまに落ちるという事象であるがゆえに再現性が低くなかなか調査が大変だったのですが、 Circle CI の build with SSH の機能などを活用して、特定することができました。
原因①: ラジオボタンがクリックできていない
さきほども言及した scenario内の一部分をリトライできるrspec-retry_exの紹介 - ENECHANGE Developer Blog の 記事でも指摘されているように ENECHANGE ではラジオボタンで、アコーディオン形式でアニメーションする画面部品があるのですが、ここでテストが失敗することが数多くありました。
このとき、 rspec の中では
find('label[for=have_bill]').click
というような書き方をしている箇所がほとんどでした。
調査の結果、上記のコードによってラジオボタンをクリックした直後、
page.find(:id, 'have_bill').checked? #=> false
となっていました。すなわち、クリックしたつもりだけど、実際にはまだラジオボタンにチェックできていない状況であることが分かりました。
対策①: ラジオボタンを確実にクリックする。何度でもクリックする。
ラジオボタンのクリックを確実に成功させた上で次に進むために、成功するまで何度でもクリックするようにしました。
具体的にはもともと
find('label[for=have_bill]').click
と書いていたところを、 choose_n というメソッドを新たに定義し
choose_n('have_bill')
と書けば、成功するまで、何度もクリックするようにしました。
choose_n メソッドは下記のとおりです。
def choose_n(test_id, n = 10) n.times do choose test_id sleep 0.1 break if page.find(:id, test_id).checked? end end
この choose_n メソッドを使うことで、
- シンプルになる
- choose メソッドを内部的に使うので、指定した要素がクリック可能な状態になるまで、待ってくれる
- checked? メソッドを使い、クリックが成功するまで何度も繰り返す
という動作をしてくれるようになります。
ただ ENECHANGE ではラベルを経由して、ラジオボタンの選択を行うコーディングになっており、何も設定せずに choose を使うと
Capybara::ElementNotFound Exception: Unable to find css
というようなエラーが発生しました。
このようなエラーが発生するのを防ぐため、下記のような Capybara の設定を行いました。
Capybara.configure do |config| config.default_driver = :selenium_chrome config.automatic_label_click = true end
この automatic_label_click = true
という設定により、 choose メソッドで内部的にラベルをクリックしてくれます。
この工夫により、劇的にテストが失敗する確率を低減することができました。
原因②: なぜかクリックする場所がズレる
ほかに「Element is not clickable at point 」というメッセージが表示され、失敗するという事象が低確率で発生していました。
この件についても Circle CI で build with SSH の機能を使って調査しました。
すると、不思議なことに click するときにクリックすべき座標と比較して y軸 下方向に 120px から 150px 程度、ズレている座標をクリックしていることが判明しました。 capybara など Ruby のレイヤでその要素の座標を取得する時点においては y軸の座標がズレていません。しかしながら Capybara::Node::Element クラスの click メソッドを呼ぶと 「Element is not clickable at point 」のエラーが発生します。そのエラーメッセージの中にある座標を調べると y軸がズレているのです。
WEB 上の資料を調べたところ、 driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
の処理を追加することで改善したというような事例もあるようなのですが、ENECHANGE ではこれを試してみても改善しませんでした。
かなり試行錯誤しました。結果として 黒魔法 Module#prepend を実践投入することで解決しました。
具体的には下記のようなコードを書きました。
module Capybara module Node module ClickByCoordinate def click_by_cordinate(x, y) js = <<~JS var x = arguments[0] - window.pageXOffset; var y = arguments[1] - window.pageYOffset; var e = document.elementFromPoint(x, y); if(e){ e.click(); return true; } else{ return false; } JS Capybara.page.execute_script(js, x, y) end def x native.location.x + native.size.width / 2 end def y native.location.y + native.size.height / 2 end def click super rescue ::Selenium::WebDriver::Error::WebDriverError => e if e.message.include?("is not clickable at point ") native = self.native native.location_once_scrolled_into_view unless click_by_cordinate(self.x, self.y) raise e end else raise e end end end class Element prepend ClickByCoordinate end end end
native.location.x
や native.location.y
はそのクリックしたいエレメントの左上の角の座標を返します。このコードではエレメントの中心の座標を計算して、 x メソッドでそのエレメントの中心の x 座標、y メソッドでそのエレメントの中心の y座標を返すようにしています。
click_by_cordinate メソッドの内部では JavaScript にある elementFromPoint という関数を使って、その座標のエレメントを取得してクリックしています。
この座標がズレる事象は低頻度でのみ発生するものであったため、まずデフォルトの処理を実行し、座標がズレた結果失敗した場合のエラーが発生した場合 click_by_cordinate メソッドを使って再度クリックするようにしました。
この結果、低確率で座標がズレ、たまにテストが落ちるという現象を防ぐことができ、Circle CI でたまに落ちる現象を防ぐことができました。
おわりに
今回は、ENECHANGE でたまに落ちるテストが発生していたことに対して、どのように対応したかについて解説しました。
ENECHANGE では Ruby on Rails エンジニアを募集しています。 積極的なご応募お待ちしております。