ENECHANGE Developer Blog

ENECHANGE開発者ブログ

ランダムにおちるfeature/system specへの対応とリトライ機構

背景

プラットフォーム事業部のtaki(@yuyasat)です。ENECHANGE社には2016年10月から参画し、主にRuby on RailsやJavaScriptまわりの実装を行っています。

ENECHANGE社の顔とも言えるサービスが、電気・ガス比較サイト エネチェンジです。WordpressとRailsでできており、リポジトリのfirst commitから4年以上が経過しています。確認したところ、first commitはCTOの白木で、2014年5月16日でした。会社の設立が2015年4月ですから、会社の歴史よりも長い歴史をもつリポジトリということになります。ENECHANGE社の前身はCambridge Energy Data Labで、その当時に開発が開始されたわけです。

そんな4年以上の歴史を持つリポジトリですが、機能も増え、申し込み可能な電力会社も増えてきており、それに伴って自動テストの数も増えてきています。また昨年Vue.jsを導入するなど、フロントエンドもリッチになってきており、絶対に落とせない画面に関してはfeature specも書いています。1

feature specが増えてくると悩まされるのが「テストが通るときもあるんだけどたまにおちる」「自分のlocalでは通るのにCI上では通らない」といったことです。100%落ちるのであればデバッグがしやすいですが、環境依存だったり、通ったり落ちたりすると原因究明に時間がかかります。通ることもあるものですから、Rebuildして通ればOKとしている開発現場も多いでしょう。

ENECHANGE社でも、直近のビジネス状況の都合上、抜本的な解決をしないまましばらくそのような運用をとっていたのですが、feature specが増えていくにつれて、いよいよRebuildしても通らないという確率が高まり、抜本的に修正しなければいけないという状況になってきました。ENECHANGE社ではCIがすべてパスしないとリリースを行わないことになっているので、CIが終わらないことで、ユーザーに素早く価値を提供できないということになってしまいます。

そこで、本腰を入れて調査をすることになりました。そして、最近fail率が落ちたのでここまでの経緯を紹介したいと思います。

調査の方法

capybara-screenshotの導入とS3へのアップロード

弊社では自動テストを回すのにCircleCIを利用しています。その際、capybara-screenshotというgemを用いてfeature specがfailした際にfail時のスクリーンショットとレンダリングされたHTMLを保存しています。system testであれば、fail時には自動でスクリーンショットを撮ってくれるので、HTMLが不要な場合は、このgemは利用しなく良いでしょう。

しかし、このままでは、CircleCI上でテストが落ちてスクリーンショットを撮ったとしても確認することができません。そのため、fail時にはS3などのストレージにあげるように.circleci/config.ymlに設定をします。CircleCI2.0では、whenオプションがあり、on_failを設定すると、失敗した時にだけS3にファイルを上げるようにします。リスト1に.circleci/config.ymlの設定例を示します。

- run:
    when: on_fail
    command: |
       sudo apt-get -y -qq install awscli # awsコマンドをインストール
       aws s3 sync tmp/capybara/ s3://circleci-failures//$CIRCLE_BRANCH/$CIRCLE_BUILD_NUM/
リスト1. テストが失敗した時にS3にスクリーンショットを上げるようにする

なお、事前にACCESS KEY IDやSECRET ACCESS KEYをセットする必要があります。詳しくはCirlceCIのDocumentをご覧ください。

S3にあるファイルをローカルに持ってこれば、失敗時の状況を把握することができます。

ローカルでの実行

次に、CircleCI上で落ちたテストをlocalで実行してみます。何回か実行してみてfailの状況がCircleCI上と同じ同様か確認。テストの順番をランダムにしている場合は、seedを指定して実行します。

CircleCIのコンテナ内での実行

次に試してみるのがCircleCI上のコンテナにSSH接続し、コンテナの中でrspecコマンドを叩き、手動で実行します。この時、CircleCIでも同時にテストを走らせてしまっている場合は、そのままコマンドを叩くとデータベースのデータが競合し、まったく訳の分からない落ち方をしてしまいますので、手動実行用のデータベースを作成するか、CI上のテストが終わるのを待ってから実行するなどの工夫が必要です。

テストの全体の数が少ない場合は、全部実行してしまえば良いですが、大抵は全部実行するのは時間がかかるので落ちたテストケースやそのテストケースを含むファイルだけを実行することが多いでしょう。ここで確実に落ちるようであれば解決への道筋は見えたも同然です。100%落ちるのであれば原因究明がしやすいからです。

単体で手動実行して、CI上では落ちていたテストがパスした場合は、まずは同じテストを何回か実行してみると良いでしょう。JavaScriptの動きがたまたま少し遅くて落ちていたのであれば、何回か実行すれば落ちるかと思います。ここから先の原因究明は、何度かfailさせて、落ち方に決まりがないか、その時のデータベースの状況はどうなっているか、specで落ちる直前の画面はどういう状態になっているかなどをbinding.pryやスクリーンショット、HTMLなどを手がかりに注意深く観察していきます。

エネチェンジでの対策

原因究明できない場合への対応

注意深い観察の結果、原因究明ができた場合は問題ないですが、feature specの場合、画面の挙動が主なテスト対象になるので - テストの順番次第では、sessionが残っていたりしてsessionに依存する画面の挙動が期待と異なる - 非同期で処理している箇所の反応がたまに悪くてタイムアウトする - フロントエンドで制御しているアニメーション等の挙動が稀にスムーズに行かなくて落ちる などで落ちることも考えられます。他にも様々な原因が考えられると思いますが、弊社の場合、フォームでアコーディオンで開く箇所がいくつかあり、多くの場合そこでfailしていました。思い返してみると私の経験でも、実家に帰省した時にマシンスペックの低い端末で操作したときに動きがカクカクしたことがあるので、CIの環境によってはそのようなことが起こるのかもしれません。

この現象は、確実に起こるという訳ではなく、うまく行くときもあれば、失敗するときもあるという状態でした。じっくり時間をかければ、失敗するときの原因が判明するかもしれませんが、新規の機能追加等の開発も行う必要があるため、この問題に取り組める時間は限られています。そこで、この対応として何回かリトライを行い、何回めかで成功すればテストにパスしたとみなす様にしました。

2つのリトライ機構

エネチェンジでは、このリトライ機構を2段階に分けて行なっています。

この2つの機構を説明するために、リスト2に簡単なfeature specの例を示します。電力会社を切り替え手続きのフォームから抜粋したもので、契約者名やお客様番号、供給地点特定番号を聞く箇所をテストするコードになります。図1のイメージになります。

scenario ‘apply an electric’ do
  vist ‘/orders/apply’
  
  fill_in "order[family_name]", with: "江根" # ・・・(1)
  fill_in “order[given_name]”, with: "替次"  # ・・・(2)

  # ラベルをクリックするとアコーディオンが開く
  find('label[for=have_bill]').click # ・・・(3)
  # アコーディオンが開かれたかどうかを検証
  expect(page).to have_selector('label[for=have_bill] + ul', visible: true)  # ・・・(4)

  fill_in "order[customer_number]", with: "12345-12345-1-12"
  fill_in 'order[spot_number]', with: '03-0123-4567-8901-2345-5678'
end 
リスト2. 電力会社切替フォームをテストするコードの例


f:id:yuyasat:20180926184715p:plain:w250
図2. リスト1でテストするフォームのイメージ

このテストでは、(3)の箇所がアコーディオンの挙動をテストする箇所になっており、アコーディオンが開かれたことを検証する(4)がランダムに落ちる箇所になります。

rspec-retryの利用

2つのリトライ機構のうち一つめはfeature specのscenario全体を再実行するリトライです。これは、リスト1において、scenarioのどこかでfailした場合、scenarioの最初からやり直すリトライです。このリトライには、rspec-retryというgemが提供されていて、こちらを利用しています。

rspec-retry_exの利用

二つめは、シナリオの中で特にfailしやすい検証部分(expect)のみを再実行するリトライです。前述のようにテストが落ちやすいのは(4)の箇所です。ここをリトライしたいがために、scenario全体をやり直してしまうと、ほぼ確実にパスする(1)や(2)の箇所までもやり直すことになってしまうので、(4)だけもう一度やり直せるような仕組みを用意しています。

具体的には、expectがfailすると、RSpec::Expectations::ExpectationNotMetErrorというErrorが発生するのでそれをrescueし、リトライする様な形を取っています。

retry_exというメソッドを実装(良い名称募集中!)し、リスト3の様な形で使用します。

retry_ex(count: 3) do
  find('label[for=have_bill]').click
  expect(page).to have_selector('label[for=already_member_n] + ul,’, visible: true)
end
リスト3. retry_exメソッドの使用例

このメソッドは、rspec-retry_exというgemにしましたので、また別の機会に紹介できればと思います。

実際の実行結果を図3に示します。1回目、2回目の試行では失敗したものの、3度目の挑戦で成功していることがわかります。

f:id:yuyasat:20180926200608g:plain
図3. retry_exで3回目に成功した例

このようにしてエネチェンジでは二段構えでfeature specのretryを実行し、ランダムにおちるテストに対応しています。

まとめ

本記事では、エネチェンジでのランダムに落ちるfeature/system specへの対応の仕方について、テストのfail時にスクリーンショットを撮って調査したり、CircleCIにログインして調べる方法を紹介しました。

また、ランダムに落ちるテストはなかなか原因究明に時間がかかります。私はある程度根気強く調べてもわからない場合は、リトライして成功すればテストにパスしたことにするという考え方もありだと思っています。そこで、リトライの方法についてもテストシナリオごとリトライする方法、検証部分(expect)のみをリトライする方法について紹介しました。

ご参考になれば幸いです。


  1. 現在、エネチェンジのRailsのバージョンは4.2系なのでfeature specを用いています。Railsのversion upも絶賛対応中!早くRails 5.1以上にしてRails標準のsystem testに移行したい!なお、他のRailsリポジトリは5系になっています。歴史あるリポジトリのバージョンアップが遅れてしまうのはENECHANGE社の屋台骨として長らく価値を提供して来たリポジトリの宿命と言えるかもしれません。