デイリー日々

生活ライフ

自動テストと初期化 - (2)並列実行性・再実行性

※続きものです。前のやつはコチラ、次のやつはコチラ

はじめに

自動テストを正しく並列実行できるよう、容易に再実行できるようにするためには、テスト開始時の初期化をうまくやることが重要です。
私の今までの経験から、どうすればうまくいきやすいか書いておきます。

並列実行性・再実行性の実際

並列実行性

並列実行ってそんなに重要?と思う方もいると思います。めっちゃ重要です。

テスト実行時間を考えたとき、直列でのみ実行していると、リニアに伸びていきます。
UIベースのテストであれば、1シナリオ30秒程度として、120シナリオで1時間になります。
120シナリオも作らない?自動化が進むにつれ120シナリオなんてすぐに到達します。
1時間くらいだったら待つ?システム全停止してhotfixを入れて、急がないとビジネス的価値を毀損するのに待つことができますか?

並列実行を前提としていれば、リソース量さえ増やせれば、実行時間は(総テスト時間) / (並列数)で済みます。
ちなみに、現在携わっているプロダクトのテストは、200シナリオ4並列で30分を切ります。分割・最適化の最中なので、それが完了すればもっと並列数を増やしてもっと短時間で実行できるようになり、CIでも軽量に回せるようになります。

そして、忘れてはいけないのは、並列実行前提で作られていないテストを並列実行するのはかなり厳しいということです。実質無理です。
実行順に依存していたり、データ構造に依存していたり、失敗する理由が大量にあります。

ということで、最初から並列実行できることを前提とした初期化システムにしておいた方がいいです。自動テストが成功するかどうか、ここにかかっていると言っても過言ではないです。

再実行性

再実行ってそんなに重要?と思う方もいると思います。めっちゃ重要です。

UIベースのテストはどんなに丁寧に作ってもFlakyです。APIのレスポンスが100ms遅延しただけでFAILします。0.1秒ですよ!
Flakinessをテストで吸収するための最も手っ取り早い方法が、リトライ設定を入れることです。不安定なテストでも3回もリトライすればそれなりに動くはずです。

しかし、ここもまた初期化の壁が立ちはだかります。固定IDでデータをinsertして初期化する仕組みにしていると、再実行時には「すでにあるよ」エラーになります。upsertでうまく避けるようにしたとしても、前回の実行で何かレコードができていると、勝手にリレーションができてしまい、表示が変わってFAILします。

さらに、テストを作っている間、動かして様子を見て変更して動かして…ということを繰り返します。そのとき、簡単に再実行できる仕組みを作っておかないと、本当に面倒です。以前はテスト実行後にDBを自動で吹っ飛ばすようにしたこともありましたが、そうするとDBコンテナが正しく立ち上がるまでテストできず、イライラしました。

ということで、最初から再実行できることを前提とした初期化システムにしておいた方がいいです。自動テストが成功するかどうか、ここにもかかっていると言っても過言ではないです。

実際どうするべきか

前回の記事の「世界」を用います。表現として好みでなければ、「状態」などと読み替えてください。

「世界」を構築する

簡単に言うと、「テスト実行ごとに独立した『世界』を作り出し、その中でテストが完結するようにする」と言うのが要点です。図にするとこんな感じです。

2並列実行してストリームが2つ生成されると、(テストランナーにもよりますが)各ストリームに適宜テストシナリオが割り当てられ、実行されます。そのシナリオごとに、「世界」が生成されます。その「世界」には、シナリオを実現できる最低限のデータを設定します。
前回の記事に倣った例(カレンダーの月曜始まり/日曜始まりをトグル)であれば、「カレンダーを表示できるユーザー」だったり、「カレンダーそのもの」だったり、それから「月曜始まり/日曜始まりを設定する何かしらのデータ」とか、です。

そして、重要なのは、これらの「世界」は全て独立しているというのが重要です。それぞれの「世界」が独立していれば、並列実行時に順序を組み替えたり、並列数を増やしたり、FAILしたときに再実行しても問題なく実行できます。

また、同じシナリオであっても、実行されるごとに別の「世界」が生成されることが大事です。テストがFAILし、再実行されたときのことを考えます。

世界1でFAILして、その再実行した世界1'が全く別のものであれば、全く別のテストと同様に実行することができます。そうでないと、例えば「カレンダーに予定を登録する」のようなステップがある場合、「世界1でFAILする前に入力した予定が世界1'で出てきてしまう」ようなことが起こりえます。こうなると、テストがFAILするのはもちろん、FAILしたときの原因の分析などが非常に面倒になります。

どうしたら実現できるか?

動的にデータを作成・挿入するに尽きます。

かつてCSVでデータを作成することも試しましたが、その場合固定IDとなってしまい、再実行時など「独立した『世界』」を初期化時に作成することができませんでした。したがって、何かしらスクリプト等で動的に作成した方がいいです。私は現状、TypeScriptで動的importを用いて実現しています。

カレンダーの例では、このような感じになるでしょうか。

  • ユーザーを挿入する: user_id を取得する
  • カレンダーを挿入する: user_id を用いる → calendar_id を取得する
  • 月曜始まり設定を挿入する: calendar_id を用いる

また、「基本的なデータをまとめておいた『世界』のスクリプト」を用意しておく、というのも便利です。そうすれば、今回のカレンダーの例は「月曜始まり設定を挿入する」くらいでOKになります。

いかがでしたか?

より具体的に「世界」を構成することによって、並列実行性・再実行性を担保し、テスト開始時の初期化をうまくやる方法について記載しました。
そして、今回記載したことは、現に私が運用して強力さを体感しています。開発エンジニアからの評価も良好です。

次は初期データ構成の勘所について書こうと思います。今扱っているプロダクトでしか実例がないので自信がない部分もありますが…まあ大丈夫でしょうたぶん〜!