2022-02-02

CypressによるE2Eテストを1年間運用してみて

1010real / okamotti

はじめに

こんにちは。フロントエンド/SETエンジニアの@1010realです。 最近車を購入したんですが、納車まで5ヶ月かかるそうです。いやはや。。

今回は、Cypressを用いたE2Eテストを自社プロダクトに導入して1年近く経過したので、その間に得たナレッジを紹介しようと思います。

(導入時の記事はこちら

目次

  • はじめに
  • 目次
  • 導入初期
  • 不安定なE2Eテスト
  • フレーキーなE2Eテスト
  • テストの高速化
  • 現在のE2Eテスト
  • ナレッジ
    • Cypressの進化は激早なので、公式ドキュメントを追う
    • テストケースを描き始める前にBest Practicesを観る
    • デバッグにはcy.debug(), debugger を使おう
    • テストを高速化するために
      • cy.wait(Number) はなるべく使わない
    • E2Eテストのフレーキーさに対応するために
      • テストケースはRetryableになるように分割する
      • after, afterEach は極力使わない
  • 今後の展望
  • 最後に

導入初期

導入したのが昨年の4月頃で、なんとかk8s上(cronjob)で定期的にE2Eを動かす仕組みができました!というレベル感でした。

初期のテストケースはログイン/ログアウト・一覧表示・商品登録のみの6ケースほどで、特に問題もなく毎朝正常に終了していました。

この頃、通知はテスト失敗時のみにしていたため、テストが成功し続けると特に通知もなく、E2Eテストの存在を忘れる日々もありました(きっと理想はそういう日々・・)

そもそもテストケースとして何が必要かをまだ洗い出せてもいなかったため、本当にただ仕組みを作っただけでした。

不安定なE2Eテスト

テストケースの洗い出しと、弊社のプロダクトにとって最低限クリティカルな機能が担保できるテストケースを追加することをOKRに設定し、半年かけて少しずつテストを追加していきました。

途中、テストケースをより簡単に追加できるように、Chrome拡張機能を自作したりもしました(機会があれば紹介したいと思います)

最低限のテストケースの8割を実装し終えたぐらいで、E2Eテストの挙動が不安定になり始めました。

テストが失敗した原因を探ろうと、テストの録画を確認しても途中からブラウザがフリーズした状態が録画されており、まともに見れませんでした。

k9sで実行中のpodを確認するとCPU、メモリともに振り切っていたため、CPU、メモリの割り当てを増やしてみましたが改善せず、cypress.logにはOOM(Out of Memory)らしきエラーメッセージがあったため、ブラウザの起動時オプションなども試してみましたが、結果的に解決できませんでした。

videoの録画をoffにしたところ、安定したため、現在もvideo録画はoffにしています。

フレーキーなE2Eテスト

最低限のテストケースを実装し終えましたが、それでもたまに何故か一部のテストが失敗します。再現しようにも一貫性がなく、2日に1度はE2Eテストが失敗して原因を探る日々が続きました。

(弊社ではこんな感じで、cypress-chanからお叱りを受けます) cypress-failed

いろいろ文献を読むにつれてわかってきたのは、画面を操作するテストという性質上、突然死は避けられないということです。いわゆるE2Eテストのフレーキーというやつですね。

(フレーキーについてはこちらのスライドが非常に参考になりました。ありがとうございます)

突然死を許容するために、1週間かけてテストケースをリトライ可能なスコープに分割しました(こちらは文書の最後にナレッジとしてまとめております)

テストの高速化

テストにかかる時間も現在のテストケースが出そろったタイミングでは30分程かかっていました(失敗が重なるともっと増えます)

現在の弊社のE2Eテストは、他のメンバーが作業していない朝の時間帯にテストを行うというサイクルなので問題ありませんが、今後、リリース前に一度E2Eで既存の挙動を担保するという理想的な状態にするには、よりテストにかかる時間を短縮する必要があります。

ずっと課題に感じていた部分ですが、つい先日、テストケースの書き方を見直すことでおよそ1/3の10分程にすることができました(こちらもナレッジとして文書の最後に書いてます) reduce-test-span

ただ、今後もテストケースが増えることを考えると、ゆくゆくはテストの並列化が必要になってきそうです。その際にはCypress Dashboardの利用も検討したいと思っています。

現在のE2Eテスト

現在はテストケース数が64まで増えました。

テストも徐々に安定実行されるようになってきました(それでも週に1度はログ眺めてますが)

また、昨年末にJOINしたエンジニアの一人が、テストに成功した時に以下のようなメッセージをSlackに投げるようにしてくれたので、テストが成功した朝はほっこりした気持ちで業務を始められるようになりました。本当にありがたい限りです! cypress-success

ナレッジ

この1年間で自分が得たナレッジを一部ですが、以下にまとめておきます。誰かの役に立てば幸いです。

Cypressの進化は激早なので、公式ドキュメントを追う

検索して出てきた記事の内容をそのまま試しても動かなかったり、既にCypressのアップデートで取り込まれている物も結構あるので、Change LogMigration Guideは継続的にチェックすることをお勧めします。(なんならcypress apiドキュメントに書かれている内容そのまま動かなくて、change logでそのAPIに変更があったことを知ることもありました)

テストケースを描き始める前にBest Practicesを観る

これからE2Eテストのテストケースを作成する方はBest Practicesを一通り眺めてから書き始めると良いと思います。後述のナレッジのいくつかはここに書いてあることの抜粋だったりします。

デバッグにはcy.debug(), debugger を使おう

これらのコマンドで、テストケース実行時にbreakpointを設定でき、より効率的にテストケース をデバッグできます。

E2Eテストは実行するのに時間がかかるため、闇雲に書き換えて実行してみるトライ&エラー方式のデバッグだと、かなりの時間を浪費します。これを知っているのと知らないのでは、テストケースの調査時間に大きく影響します。

※chrome developer toolを表示していないと動かないので注意が必要です。

詳しくはこちら

テストを高速化するために

cy.wait(Number) はなるべく使わない

固定で数秒待つというwaitを入れるとテストがものすごく遅くなるのでできる限り避けます。

詳しくは こちら に書いてありますが、

  • cy.visit()はwindow.onloadまで勝手に待ってくれる
  • cy.get()はdefaultWaitTimeの時間だけ、要素の表示を待ってくれる

という仕様のため、cy.waitを使う必要はありません。 ただし、以下の場合は自前でwait処理をする必要があります。

  • 要素の構造的な変化がない場合

同一レイアウトでデータの更新を待ってから処理したい場合などでは、特定のAPIリクエストの終了を待つようにします。

cy.intercept('GET', '/api/bff/v1/subscription/supplier?q=*').as(
  'getSupplier',
)
cy.get("[data-testid='j-subscription-order-selector-search-word']").type(
  'SS商店\n',
) // 入力と同時にapiが叩かれる
cy.wait('@getSupplier')
cy.get('td:nth-child(1)').first().click() // テーブル(検索結果)の一番先頭を選択

E2Eテストのフレーキーさに対応するために

テストケースはRetryableになるように設計する

E2Eテストなので複数画面を行き来する、ユーザストーリーに沿った一つの大きなテストケースを書きたくなりますが、実際に画面を操作して行うテストという性質上、サーバが重くてリクエストがタイムアウトするなど、外部要因によるエラーが結構な頻度で起きます。

そのため、E2Eテスト実行時には、何回かRetry設定するのが一般的ですが、テストケースがRetryableでない場合には、Retry時のテストが絶対に失敗してしまいます。

弊社ではRetryableにするために、トランザクション毎にテストケースを分割しました。

あまり参考になる例ではありませんが、弊社のケースを例を示します。

以下は 商品と仕入先を登録後、在庫登録でそれらを選択して登録 というシナリオに対するテストについてです。

  • 非Retryableなテストケース
it('商品と仕入先を登録後、在庫登録でそれらを選択して登録', ()=>{
  cy.visit('/subscription')
  {商品登録画面に遷移して商品登録}
  {仕入先登録画面に遷移して仕入先登録}
  {在庫登録画面に遷移して各ステップ入力後在庫登録}
})

上記のテストケースでは、在庫登録時にレスポンスエラーが起きた場合に、商品登録からリトライすることになりますが、その際、入力内容に一意な項目が含まれていると登録に失敗してしまいます。

ちなみに弊社のテストケースにおいて、1つのspecファイル内で使用する商品IDはテスト開始時に採番され、システム的に商品IDは一意である必要がありました。なので2回目以降の商品登録では、必ずDuplicate keyエラーが起きていました。

なので、以下のように分割します。

  • Retryableなテストケース
it('商品登録画面に遷移して商品登録', ()=>{
  cy.visit('/subscription')
  {商品登録画面に遷移して商品登録} // 登録完了後のページURLは /product
})
it('仕入先登録画面に遷移して仕入先登録', ()=>{
  cy.visit('/product')
  {仕入先登録画面に遷移して仕入先登録} // 登録完了後のページURLは /supplier
})
it('在庫登録画面に遷移して各ステップ入力後在庫登録', ()=>{
  cy.visit('/supplier')
  {在庫登録画面に遷移して各ステップ入力後在庫登録}
})

これで、在庫登録中に失敗したとしても、在庫登録の最初からリトライするため成功します。

(beforeEachで、データのリセットを行うことも一つの方法だと思いますが、稼働し続けている環境のDBを直接操作することは避けたいため、弊社ではこのような対応をしております)

after, afterEach は極力使わない

これも こちら に書いてありますが、 after, afterEachが開始時の状態は、実際のテストケースの実行結果に依存するため、実行時の状態がいつも同じとは限りません。

また、テスト失敗時にもafterEachの処理が実行されるため、そこで処理に失敗するとリトライ処理が重複して行われ、テスト結果が壊れます(Cypress 8.7.0の話です)

これから開始するテストに対する状態のリセットは、基本的にbefore(Each)で行いましょう。

今後の展望

今は毎日定時にE2Eテストを実行していますが、テストによるフィードバックはなるべく早い方が良いため、CIへ組み込みにチャレンジしていく予定です。

さらなる高速化と環境整備が必要となるため、とてもワクワクしています笑

さらに、プロダクトを今以上にスケールさせていくために、プロダクトの品質管理を行うSET/QAチームの立ち上げを予定しており、エンジニアを絶賛募集中ですので、興味ある方は是非こちらをご覧ください。

最後に

昨年はE2Eテストに始まりE2Eテストに終わる1年でしたが、これを始める前と後では明らかに知識も視点も変わったと思います。特にテストに関する知識と、インフラに関する知識を得られました。

今年もプロダクトの信頼性を高めるために色々なチャレンジをしていこうと思います。

最新の記事