こんにちは、ホワイトプラスでLenetのアプリとwebのフロントエンドを開発している@akaimoです。
本日はLenetのwebにおけるE2Eテストの取り組みを紹介したいとおもいます。
Lenetとは
どのようなところにE2Eを導入したのかイメージしにくいと思うので、簡単にサービスを紹介します。
Lenetとは、インターネットで注文できる宅配クリーニングです。クリーニングに出したい日と受け取りたい日を選択するだけで、指定した日に集荷・配達してくれます。最近は宅配ボックスからの集荷もできるようになったので、家にいる必要すらありません。ぜひ一度使ってみてください。
E2Eを導入した理由
Lenetの要である注文フォームはReact+Reduxで開発しており、新規ユーザー用とリピーター用で、日付選択のコンポーネントなど様々な部分を使いまわしています。むしろ、一部の表示を消しているだけで、ほとんど同じと言っても過言ではありません。
こうなってくると、新規ユーザー向けに機能を拡張したつもりが、リピーター用で予期せぬ影響をだしてしまうことが多々あり、「違う画面だから」という事で受け入れテストでも発見できずにリリースされてしまうこともありました。
そんな状態でも機能は増え続け、ユーザーの種別によって機能を切り替えることもあり、手でテストするのが限界になってきたのでSeleniumによるE2Eテストを導入しました。
E2Eの構成
LenetのE2EではSeleniumとブラウザの環境が整ったDocker imageを用意し、同じ環境で繰り返しテストできるようになっています。テストコードは、「サクッと書けるから」という安直な理由でPythonを選びました。
また、LenetではCIが自動デプロイの一部を担っているため、E2Eテストを行うとリリースに時間がかかってしまうことと、導入初期は不安定だったこともあり、E2Eテストをpushやmergeに対して行っていません。
現在は、
- Circle CIのAPIを利用した深夜の定期な実行
- 危なそうなリリースをするときに各自で実行
の2パターンでテストしています。
このままだと、テストがあるのに「リリース前に実行されなかったために気づけない」なんてことが起こってしまいかねないので、自動化の環境を構築しているところです。
出てきた課題
構成以外で感じた課題がこちらです。
- 何も変更してないのにテストが失敗することがある
- テストが失敗したときに、状況がわからない
- ちゃんとテストできていない
3.1. 固定の値を使っている
3.2. DBに特定のデータが入っていることに依存している
3.3. テストの順番に依存している - このままだと、実行時間がすごいことになりそう
今回は、この中から1と3についてお話します。
2はスクリーンショットを取れば解決し、
4は並列で実行できる環境とテストコードを用意すればよさそうですので、自動実行と合わせて別の機会にお話できればと思います。
テストが不安定
散々言われていることですが、E2Eはユニットテストとは異なり、ネットワーク、DB、CPUの仕様状況によるブラウザの反応の差、などの要因により不安定になりがちです。
よくある方法が、「テストが失敗したらもう一度そのテストを実行してみる」というものですが、再びテストするのは時間がかかりますし、失敗するかもしれない状況が個人的に嫌だったので、別のアプローチをとりました。
失敗させない
ほんの少しだけアプローチを変え、予定していたアクションが失敗したら即テストが失敗するのではなく、アクションが失敗したら少しのインターバルを挟み、もう一度そのアクションを実行するようにメソッドをラップし、それを実行するようにしました。
以下クリックの例です
# 通常のやり方
element = driver.find_element_by_id('hoge')
element.click()
# リトライ
def click(self, element, c=1):
try:
element.click()
except WebDriverException:
if c == 5:
raise Exception("クリックに失敗しました")
time.sleep(c)
self.click(element, c+1)
element = driver.find_element_by_id('hoge')
self.click(element)
このようにするだけで、Seleniumのエラーによるテストの失敗はなくなり、とても安定するようになりました。
しかし、100回テストして100回とも同じ結果を返すかと言われると、なかなか難しいところではあります。
LenetのE2Eで、この書き方に変えてから3ヶ月ほど運用してきましたが、DBとの接続がうまくいかなかったことなどによる失敗が1,2回発生してしまっています。テストの失敗に慣れてしまうほど頻繁ではありませんが、失敗に対する疑念の元になってしまうのでゼロにしていきたいところです。
外部サービスが動いていない(例えば決済サービスのサンドボックスが落ちている)などを除き、自分たちが管理している部分であれば、同様のアプローチでエラー処理をしていくことにより、本来失敗すべきではないところでの失敗はゼロにできると思うので、取り組んでいきたいと思います。
正しくテストできていない
冒頭でお話したとおり、自分はGUIの開発を主としていたためテストに対する知識がほとんどありませんでした。そんなとき@Kuniwakさんの発表でxUnit Test Patternsという本を知り、テストの書き方について勉強をはじめました。
E2Eとユニットテストでは色々と異なるところがあり、そのまま適用することはできませんが、テストの基本となる部分は同じなので、それを意識しながら書き換えていきました。
テストで使用するModelやデータのCreatorなど、オブジェクトを意識するようになるにつれ、Pythonでテストを書くのが大変になってきました。
そこで、まだ大したコード量でもないし、どうせテストの仕方を変えるなら、言語を変えてしまっても大差無いのでは?と考え、思い切ってPythonからKotlinに変更しました。
Seleniumの言語サポートにKotlinの名前はありませんが、Javaと100%互換を謳っているので問題なく動きます。
KotlinのE2E環境
つい先日Kotlinに書き換えはじめたので、最新すぎる環境になってます。
- Java9
- Kotlin 1.2.10
- JUnit 5.1.0-M1
- Selenium 3.8.1
Java9を選択したため、JUnitがM1になってしまいましたが、まぁOKです。
ライブラリを含めた状態でjarファイルにしておき、Dockerでcliツールのようにjarを実行してテストします。
環境構築について今回はふれませんが、調査のついでに作成したリポジトリを載せておきます。
akaimo/kotlin_selenium_sample
Kotlinで書き換えてみて
まだ少ししかKotlinになっていませんが、静的型付け+型推論のコードの書きやすさはすばらしいです。また、Pythonと比較して抽象化がやりやすいことは大きなメリットだと思います。パターンマッチで型を意識したコーディングができるのもいいですね。
今後の生産性は間違いなく上がるでしょう。
デメリットとしては、環境構築が少し複雑なところぐらいです。さすがに手軽さではPythonには敵いません。
E2Eを導入してみて
まだまだ開発途中ではありますが、以前に起こった不具合や、同様に起こりそうなことのテストがあると、リリース前の不安な気持ちが和らぎ、自信を持ってリリースに望めます。
今後予定しているReactのバージョンアップなどでも活躍しそうです。
また、小さいものですが不具合を発見したこともあり、社内でのE2Eテストの価値は高まってきています。
E2Eテストを書く価値はあるといって良いでしょう。
まとめ
LenetにおけるE2Eテストの導入から現在に至るまでを紹介しました。
テスト、とくにE2Eテストは難しいところが多く、安定稼働のハードルが高いのは事実ですが、それに見合ったリターンを得ることができます。
いきなり完璧を目指すと、とても長い道のりになってしまうので、少しずつ重要な画面からE2Eテストを始めてみてはいかがでしょうか。