この記事では、CircleCI を使ってテストを自動実行できたら、次のステップとしてテスト実行時間を短縮するためのテクニックである テスト分割と並列実行 についてご紹介していきます。

テスト時間を短縮する目的は?

ソフトウェア開発において、テストが重要であること自体は、説明を必要としないものと思われます。ただし、少し考えてみると、世の中にある「テスト」は少しずつその目的が異なるように思われます。

例えば、学校のテストは授業で学んだことの定着度合いを学生ごとに確認する、また、テストによって、先生の授業の教え方や進め方が学生たちに合ったものであるかどうかを、比較検討可能な数値として確認することができるようになります。

一方、工場におけるハードウェア製品のテストは、同じ生産ラインで同じ部品(一定の許容誤差の中に収まる部品)を使って生産された製品が、同じように仕上がっているか(一定の許容誤差の中に収まっているか)を確認するために行われます。

これに対し、ソフトウェアのテストは、開発されたソフトウェアが仕様通りに動作するか、とりわけ新規開発時の動作であったり、ソフトウェアの機能追加や修正時であっても、仕様が変更されていない限りは、以前と同様に動作するかどうかを確認するために行われます。

ハードウェア製品の生産において問題が発生するのは、設計に問題がある場合と、生産に問題がある場合の双方が考えられる一方で、ソフトウェアの場合、コードを書くそのタイミングでバグが埋め込まれるのです。

したがって、ソフトウェアの開発においては、コードを作成、修正した後、できるだけ早くそのコードをテストし、バグの存在に早く気がつくことができれば、

  • そのバグの原因となるコードの範囲のあたりがつけやすい(問題箇所を極小化できる)
  • 実際にそのバグを修正する上で、開発者の頭の中にコードのロジックや変数の記憶が薄れる、曖昧になる前に対応できる

といったメリットが挙げられます。

テスト時間を短縮する方法は?

テスト時間を短縮するには、2つのアプローチが考えられます。1つはテストを実行するマシンのスペックをより高スペックにするアプローチで、これは、個人のPCのスペックを高スペックにすることで(ある程度までは)開発生産性が向上することに対応します。

もう1つのアプローチは、テストの並列実行です。これは、テストを実行するマシンのスペックを上げるのではなく(むしろ、多くの場合は下げてもよい)、テストを実行するメンバー(マシン)を増やし、同時に実行することです。テストにおいて相互依存がなければ、1人(一台)よりは2人(二台)でテストすることでテスト時間の半減、3人(三台)であれば三分の一になることが期待されます。

継続的インテグレーション(CI)を開発プロセスに取り込み、ビルドやテストが自動化されているからこそ、人に頼んだり、物理的なハードウェアを買い増すことなく、並列実行の恩恵を簡単に受けることができるのです。

テストの分割と分割したテストの並列実行

ここで、test/java ディレクトリ配下に Java で書かれたテストクラスのソースコードが格納されている場合を例にとって考えてみます。

テストクラスのソースコード

ここでは、net.mayoct.test パッケージに、FifteenSecondTest.java や FiftySecondTest.java といったテストクラスが用意されています。なお、ここでは説明のためにクラス名がテスト所要時間を示していますが、(当然) CircleCI はこのクラス名からテスト所要時間を予測しているわけではありません。

さて、test/java ディレクトリから、

circleci tests glob "**/*.java"

を実行することで、配下の任意のパッケージに属する *.java ファイルの一覧が抽出されます。

  • net/mayoct/test/FastTest.java

  • net/mayoct/test/FifteenSecondTest.java

  • net/mayoct/test/FiftySecondTest.java

  • net/mayoct/test/FiveSecondTest.java

  • net/mayoct/test/FortyFiveSecondTest.java

  • net/mayoct/test/FortySecondTest.java

  • net/mayoct/test/HundredSecondTest.java

  • net/mayoct/test/LibraryTest.java

  • net/mayoct/test/SixtySecondTest.java

  • net/mayoct/test/TenSecondTest.java

  • net/mayoct/test/ThirtySecondTest.java

  • net/mayoct/test/TwentySecondTest.java

circleci tests splitですが、パラメーターに --split-by=timings を指定することで、過去のテスト実行時のタイミングデータが存在する場合、各グループでの実行時間ができるだけ均等になるような分け方をしてくれます。

実際には、並列数として指定された分の各コンテナ内で circleci tests split を実行することで、(CircleCI を使用するユーザーが意識することなく) 当該コンテナで実行すべきテストの一覧が取得できるようになっています。

テストの一覧を取得

したがって、得られたテストだけを実行すれば、最終的には並列実行数分の全コンテナによりすべてのテストが実行されることになります。

なお、タイミングデータを保存するには、store_test_results ステップを(テスト終了後の適当なタイミングで)実行する必要があります。

store_test_results ステップ

テストの自動分割と並列実行の効果

テストの自動分割と並列実行により、テスト所要時間はどの程度、高速化されるのでしょうか? また、高速化、つまり並列実行により CircleCI の使用クレジット(つまりコスト)にはどの程度の違いが出るのでしょうか?

こちらの図をご覧ください。

![store_test_results ステップ]effect-of-parallell-execution.png)

全部で10個のテストクラスを1並列、3並列、5並列で実行した際の所要時間と、使用クレジット(リソースクラス medium指定時)をまとめたものです。

1並列と比較し、3並列では実行時間は三分の一強(並列実行に伴うオーバーヘッドのため)、5並列では四分の一強(オーバーヘッド+組み合わせ方のため)といった具合にテスト実行時間が短縮化されています。

その一方で、コストに関しては、並列実行に伴うオーバーヘッドもあり、1並列と比較すると、3並列では16%、5並列では27%高くなっているものの、実際にはテストの数が増加するほど、オーバーヘッドの占める割合が小さくなる、つまり、コスト面での違いが小さくなるものと見込まれます。

さいごに

テスト時間が短縮するということは、ソフトウェアを開発する側から見れば、何らかのバグが新たにコードに組み込まれたとしても、そのコードを追加したのは遠い昔、その頃の記憶を呼び戻して(不確かな記憶のまま)修正に取り掛かる、といったリスクを回避できます。これは開発効率(量)の向上であると捉えることができるでしょう。

それだけにとどまらず、ワークフローの実行時間の短縮をゴールにするのではなく、短縮によって生まれた時間を原資に、セキュリティスキャンなどを組み込むことで、ソフトウェアの質の向上との両立を可能にします。

CircleCI にはビルドやリリース、リリースやデプロイ、セキュリティの質を向上するさまざまなツールを自動化ワークフローの中に簡単に取り込むための仕組みである Orb があります。テストの自動分割、並列実行+Orbの活用で、ソフトウェア開発の質と量の双方を高めていきましょう。

関連記事