サバンナ便り ~ソフトウェア開発の荒野を生き抜く~

第3回テストサイズ ~自動テストとCIにフィットする明確なテスト分類基準~

テストの分類として開発者に馴染み深いのは、検証の対象となるコードの範囲や粒度での分類でしょう。範囲が狭く粒度が細かい順に、ユニットテスト、インテグレーションテスト、E2Eend to endテストなどと呼ばれます。今回は、自動テスト前提の時代にうまくフィットするテスト分類について考えます。

現場の混乱

実は、範囲や粒度による分類に現場は混乱しがちです。⁠1つの対象」を検証する狭いテストをユニットテスト、単体テスト、コンポーネントテストなどと呼びますが、これらをほぼ同じものと言う人も、異なると言う人もいます。⁠1つの対象」も関数、メソッド、クラス、モジュール、パッケージ、振る舞い、1つの画面と、人や組織によってバラバラです。

複数のレイヤ、たとえばコントローラとモデルをまたいで検証するテストをインテグレーションテストと呼ぶ人もいれば、それもユニットテストと呼ぶ人もいます。ユニットテストはファイルシステムに触ってはならないという人もいれば、データベースに触ってもよいという人もいます。こういった混乱は、組織や時間をまたいだ知見の共有を妨げます。

テストサイズとは何か

そこで、近年注目される自動テストの分類がテストサイズです。これはGoogle社内から広まり始めた分類で、自動テストに使用されるリソースの量や実行場所、実行時間に注目します。テストサイズにはSmall、Medium、Largeがあります。小中大というわけですね(Googleにはさらに巨大なサイズEnormousもありますが、本稿では割愛します⁠⁠。

単一のプロセス内で動作するテストをSmallテストと言います。非常に高速に動作し、かつスケールしますが、単一プロセス内で動作させるので外部リソース(ネットワークやデータベースなど)は使えません。プロセス外への通信はテストダブル(MockやStub、Fakeなど)で置き換えます。動作速度目標は1件100ミリ秒未満で、最長1分で強制終了されます。

Smallテストの制約を緩和し、単一のマシンに閉じた環境であれば外部リソースの利用を許容するテストをMediumテストと呼びます。たとえば、コンテナ技術を使えば単一のマシンの中でもデータベースサーバやWebサーバを立ち上げて通信するテストもできます。動作速度目標は1件1秒未満で、最長5分で強制終了されます。

Mediumテストの制約をさらに緩和し、自動テストからリモートマシンへのネットワークアクセスなども許容するテストをLargeテストと言います。本番環境やそれと同等のインフラを利用したテストなどが相当します。動作速度目標は「できる限り高速に」ですが、通常は1件数秒から数分かかります。最長15分で強制終了されます。

明確な基準を備える

短くまとめると、Smallテストは単一のプロセス内、Mediumテストは単一のマシン上で実行され、Largeテストの実行には制約がありません。

テストサイズとテストの範囲や粒度は独立した概念ですが、おおむね相関しています。多くの場合ユニットテストはSmallテストでもあります。ただ、現場で混乱が生じがちなあいまいな領域でこそ、テストサイズの明確な基準が活きてきます。

たとえば、コントローラ、モデル、ビューをまるごと実行範囲に入れていても、データベースをテストダブルで置き換えていて単一のプロセス内で動作するテストはSmallテストです。モデルのメソッド1つだけのユニットテストでも、テストの実行にローカルホストの実データベースとの接続が必要なものはMediumテストというわけです。

Webシステムの場合、言語の実行環境だけで動かせるのがSmallテスト、Dockerのコンテナなどを組み合わせて動かすのがMediumテスト、本番同等の環境にデプロイして動かすのがLargeテストと考えるとイメージしやすいでしょう。

CIとの親和性が高い

現代の開発で重要なのは、質の高いソフトウェアをより高頻度でより確実に提供できるように複数のフィードバックループを形成することです。そのために、コミットが自動的にビルドされ、さまざまな環境でのテスト実行結果が報告され、マージやデプロイ、リリースなどの判断を支えるCIContinuous Integration、継続的インテグレーション)を行います。

CIの目的の一つが、問題のあるコード変更をできるだけ早くかつ自動的に捕捉することです。その実現には、高速で信頼性の高いテストが鍵になります。

この目標にテストサイズがフィットします。Smallテストは高速かつ決定性のある動作をします(コード以外にテスト失敗の要因がありません⁠⁠。プロセス内で完結しているので互いに独立しており、並列実行も容易です。開発者の手もとでもCI環境上でも頻繁に実行でき、テスト失敗時もどこが原因かすぐにわかります。その代わり、Smallテストが成功しても本番環境で同様に動作する保証は得られません。Smallテストはプロセス外とのやりとりをテストダブルで置き換えているからです。

Largeテストは通信先のサービスやデータベースなどに本物を使うので本番環境で動作する自信を得られます。その代わり、準備が難しく、動作は遅くかつ非決定的になりがちです。テスト失敗時も原因の分析は難航します。実はコードに問題はなく、実行が不安定なだけというようなテストは信頼不能テストflaky testと呼ばれ、偽陽性を招きます[1]

実行しているテスト環境が本物の挙動を反映している度合いを忠実性fidelityと言います。テストサイズは、⁠速度および決定性」「忠実性」との間のトレードオフを意識したテストの設計および分類法と言えます。基本的には、Smallテストの比率を上げ、Largeテストの比率を可能な限り下げることが、高速で安定した開発を支えます。

自動テストを常に実行し続ける時代では、CIのワークフローのどこでどのような自動テストを実行するかが重要です。自動テスト群をテストサイズでフィルタリングできれば、ワークフローの最初のほうで小さいサイズのテストのみを実行し、後半でより大きいサイズのテストを実行できます。これにより、できるだけ早期に効率的に問題を把握し、チームの判断や自信を支える、速度と忠実性のバランスがとれたワークフローを構築できます。

テストサイズを支えるスキル

Smallテストでは、自分で書くテストダブル(Mock、Stub)を使いすぎると、実装の変化に弱くなり中長期的な保守性が損なわれます。そのため、責務の境界やプロセス外とのやりとりに着目し、必要十分な量のテストダブルを適切な箇所に使用する自動テスト実装スキルが求められます。

Mediumテストでは、単一マシン内でテストの忠実性を高めるために、第三者が開発した代替実装(Fake)やコンテナなどを組み合わせた自動テスト実行環境を構築するスキルが求められます。

Largeテストでは、不安定な外部ネットワーク通信や、ゼロリセットができず、ほかのテストの実行とも干渉する環境などに起因する非決定性を可能な限り抑えるテスト実装スキルが求められます。

テストコードにテストサイズを紐付ける方法は、テストのタグ付け[2]や、ディレクトリ名やファイル名、関数名への命名規則適用などが挙げられます。

おわりに

基準が明確でCIとも相性が良いテストサイズを活用し、開発を効率化していきましょう。

おすすめ記事

記事・ニュース一覧