▶️

E2E テストの同時実行が可能な環境を構築した話

2024/12/27に公開

こんにちは。ナレッジワークの torii です。
ここ最近は E2E テスト基盤の改善に力を入れており、この記事ではその中でも特に成果の出た改善を一つ紹介します。各社それぞれテスト事情は異なると思いますが、何かの参考になればと思います!

前提

ナレッジワークでは Playwright を使って E2E テストを書いています。テストは開発環境で実行しており、毎朝 CI で実行した結果を Slack に通知する仕組みが構築されています。また、テストはフロントエンドエンジニアと QA エンジニアが協力して書いています。

同時に複数の人がテストを実行できない問題

会社とプロダクトが成長するにしたがって E2E テストの書き手は増えていきましたが、徐々に問題が起こるようになりました。複数の人が同時にテストを実行すると、テスト同士が干渉しあってテストが落ちてしまう問題です。

(製品としての)ナレッジワークは B2B の SaaS であり、お客様(組織)ごとにテナントを分けて提供しています。当時、開発環境には E2E 用のテナントが2つ用意されており、E2E テストはこのテナントをターゲットに実行されていました。
さて、もしここで2人が1つのテナントで同時にテストを行ったらどうなるでしょうか?片方の実行でデータが作成され、もう片方の実行でそのデータが削除される...といったことが起きてテストが落ちてしまうのです。

このテスト実行の干渉を避けるため、日々 Slack で「誰か今 E2E テストを実行していますか?」「実行中なのでもう少し待ってください!」というやり取りが行われている状況でした。とても効率が悪いし、ストレスも溜まりますね!また、将来的にはデプロイのフローの中にも E2E テストを組み込んで流す予定があり、この問題がさらに大きくなることが予想されました。

「テナントプール」というアイデア

ここからは解決策の話になるのですが、まず没になった案を潰しておきましょう。

  • 動的にテナントを作成して実行する: 作成に時間がかかる+手動の設定が必要なため NG
  • 人数(× 並列実行数)分のテナントを作成して自分のテナントを使うようにする: 大量のテナントを作成する手間がかかるため NG

最終的に、「テナントプール」という仕組みを考案して実装することにしました。

テナントプールの仕組み

仕組みはざっくり以下の通りです。

  • あらかじめある程度の数のテナントを作成してテストが実行可能な状態にしておく
  • テスト実行時に使用可能なテナントを自動で選択する
  • テスト実行中はテナントをロックしておき、終了時にロックを解除する

データベースの接続プールに馴染みがあれば、それと似た仕組みと思えば理解しやすいかと思います。

実装としては、 playwright コマンドの実行前にテナントを選択するコードを挟みます。

const TARGET_TENANT = await $`pnpm exec choose-tenant.ts`; // ここで使用可能なテナントを選ぶ

await $`TARGET_TENANT=${TARGET_TENANT} pnpm exec playwright test`;

ちなみに上のコードは zx を使っています。最初はシェルで書いていたのですが、途中でこのライブラリを使うのが便利だと教えてもらいました(感謝)。

テナントプール構築までの道のり

仕組みは 3 行で説明できるほど簡単なのですが、実装と新方式への移行に関しては考えることが結構ありました。ここからはひたすら苦労話をしていこうと思います(笑)

テナントのセットアップ手順をドキュメント化

テナントプールに十分な量のテナントを用意するため、テスト用のテナント作成に必要な手順を調べる必要がありました。テナント作成自体は管理画面からすぐに出来るのですが、作成さえすればすぐにテスト出来るわけではなく、以下のような作業が併せて必要だったためです。

  • 有効にするプランを正しく設定する
  • テストに必要なユーザーを追加する
  • ユーザーにライセンスを割り当てる
  • 常に必要なオプション機能を有効にしておく
  • 外部サービスとの連携を済ませておく
  • その他

幸いなことに、元々テスト中に使用したデータは global-teardown で全て削除する構成にはなっていたので、テナントに存在するデータに強く依存しているということはありませんでした。とはいえ、それでも中には上のような事前条件を必要とする場面がありました。
難しかったのは、どこに暗黙の前提が隠れているのかが分からなかったことです。そこで、プロダクトの各領域に詳しい人が集まって何度もテストを実行しながら地道に前提条件を洗い出して、セットアップ方法をドキュメントにまとめあげました(大変感謝)。

セットアップの半自動化

次に、ドキュメント化した手順に沿ったセットアップを可能な限り自動化していきました。と言っても、全てを自動化するのは難しかったので「半自動化」という手段を取ることにしました。

例えば、外部サービス連携で多要素認証が必要になった時、認証部分だけ手動で突破して残りは自動でやりたいことがありました。そのような場合、 page.pause() を使うと、実行を一時停止して手動ステップを挟めることに気づきました。

// テスト用のアカウントでログインしてください
// 完了したら |> を押してください。
await page.pause();

(一時停止した時に UI に周辺のコードが表示されるので、コメントの指示が読めて便利です。)

連携するサービスによっては画像認証などが気まぐれに出現することがあり、 page.pause() を正確に実行するのにも苦労しました。

正しくセットアップできているかをチェックする

テナントプールでは空いているテナントがランダムで選ばれるため、全てのテナントが全く同じように振る舞うことが重要です。ここを曖昧にしておくとテナントプール自体の信頼性が落ちてしまいます。
しかし上で見たように手動のプロセスもいくつか入っているため、 global-setup でテナントが正しくセットアップできているかをチェックすることにしました。

ロック機構の実装

実行中のテナントをロックする機構も作りました。それ専用の API を実装しても良かったのですが、そこまでする必要もないと思ったので既存機能を使ってロックを実現しました。具体的に言うと、テナント選択時に tenantlock という名前のグループ(ユーザーが所属するもの)を作成して、テスト実行後にそれを削除します。グループを作成する API は「同名のグループがあった場合はエラーを返す」という都合の良い性質があり、ほぼ同時に実行しても上手く動作してくれました。

難しかったのは、テスト実行を中断した時にロックしっぱなしになったことです。実行中にターミナルで Ctrl+C を入力した時に SIGINT のトラップに失敗することがあるのと、上手くトラップ出来たとしてもそこで非同期処理を実行できないという困難がありました。

process.on("SIGINT", () => {
  // ここで大したことはできない
});

この問題は未だ解決できていないため、一旦以下の方法で回避しています。

  • tenantlock グループを作成したのが自分自身であると推定できる場合はロックを無視する
  • tenantlock グループが作成されてから1時間以上経っている場合はロックを無視する(現状テストは1時間以内に終わる想定のため)
  • ロックを解除して問題ないという確信があれば tenantlock グループを手動で消しても OK とする

その他の工夫

プール内のテナントは全て平等であるため、名目上はどのテナントで実行しているのか意識しなくて良いことになっています。とはいえ、何かの拍子に特定のテナントだけ別の動きをするようになることがないとは言えません。
そこで、 CI から Slack への通知にどのテナントが選ばれたかを表示したほか、実行オプションとして特定のテナントを指定して実行できるようにしました。

改善の成果

以上の対策により、現在は無事に干渉なく E2E テストを実行できるようになりました。現在は開発環境で 4 つのテスト用テナントが稼働しています。また、テナント作成手順の多くの部分を自動化したため、他のメンバーも安全にテナントを作成できるようになりました。

ただ、移行直後は今までの癖から「今から E2E 実行します〜」というメッセージが Slack に流れてきたりしました(笑)

最後に

この記事の内容の一部は、過去にナレッジワーク主催の勉強会「Encraft #18 Frontend のテスト全部知る 〜Unit Test から E2E まで〜」(開催レポート)でも話しているので、もし興味があればそちらもご覧ください。この時(9/20)は「そろそろ完成します!」と言っていましたが、気づけば 12 月になってしまいました。
何はともあれ形になってホッとしています。設計から実装まで丁寧にレビューしてくださった同僚諸氏に感謝です!


【募集】現在エンジニア積極採用中です!興味を持たれた方、是非カジュアル面談でお会いしましょう。

株式会社ナレッジワーク

Discussion