要約
- ユニットテスト(xUnitで実装されたテスト)は、技術的にはコントローラー層のテストも可能ですが、テストサイズの観点からDB/外部APIアクセスを含む部分は避けるべきで、モックを使ってサイズダウンする工夫が必要です。
- ユニットテストで最も効果が高いのは変更頻度の高いビジネスロジック層であり、変更が少ない層やAPIテストに任せられる層よりも、この中間層を厚くしましょう。
- テストトロフィーの「インテグレーション層を厚く」という主張は、実はフロントエンドにおけるSociable Testsのことを指しており、WebAPIテストを増やす意味ではなく用語の定義が異なるだけで、テストピラミッドの「ユニットテストを厚く」という考えに収束します。
はじめに
この記事は、 ソフトウェアテスト Advent Calendar 2025 の19日目の記事です。
会社のアドベントカレンダー も書いたので、ご興味あればこちらもご覧ください。
さて、ユニットテストのテストコードも見ているQAの皆さんはどれくらいいらっしゃるでしょうか?世の中の大半のプロジェクトでは、ユニットテストのテストコードのオーナーシップは開発者が持つものなので、QAがユニットテストに触れる機会は殆ど無いかもしれません。それ故か、もっぱら開発者の間ではユニットテストの話題はちょくちょく上がるものの、QA界隈ではユニットテストの話題が上がることは少なめで、ユニットテストに対する理解不足や誤解もちらほらと見られます。
では現在のネット記事でQAがユニットテストへの知見を深めることは出来るでしょうか?前述の通り開発者界隈ではユニットテストの記事はある程度書かれていますが、大抵はテストコードの書き方に留まっており、正直に言うとQAの方にとっては興味関心からズレていることでしょう。例えば他のテスティング手法との違いの方が興味関心は高いのではないかと私は思っています。そこで今回は、E2E/APIテストと比較しながら、ユニットテストのカバー範囲に着目したいと思います。更に、ユニットテストが実装可能な範囲、実装しない方が良い範囲、より効果が高い範囲について考察し、そこからテストピラミッドとテストトロフィーの関係性について記述したいと思います。
用語について
「ユニットテスト」という用語は多義に渡っているので、先に定義づけをしておきましょう。各プログラミング言語でのテスティングフレームワークをxUnitと言いますが、このxUnitに実装されたテストを「ユニットテスト」と本記事内では定義します。後述するテストサイズの概念からユニットテストを定義付ける方法もあり、その定義では本定義とズレが生じますが、ご了承ください。
本論
E2E/API/Unitテストの範囲
各テストのカバー範囲を理解するには、システムのアーキテクチャの理解が不可欠です。ユニットテストは、バッチ処理といった単体で動く簡易的なプログラムにも適用出来るテストではありますが、今回はブラウザで動くウェブアプリに着目しましょう。我々が使っているウェブアプリ(このQiitaも含めて!)がどのように動いているかをシーケンス図で表してみて、そこから違いを見ていきましょう。
こちらが一般的なウェブアプリのシーケンス図です。必ずこの流れに沿うというわけではないですが、今回の図の流れを簡単に説明すると以下のような流れになります。
- ユーザーが画面上のボタンをポチッと押す。
- ユーザーのアクションを受け取ったブラウザは、HTML(あるいはJavaScript/TypeSript(JS/TS))に埋め込まれたリンク先にリクエストを飛ばす。
- リクエストを受け取ったバックエンドサーバーは、サーバー外にあるDBにSQLクエリを実行して、データを取得する。
- データを受け取ったサーバーは、データを加工する。
- データ加工を終えたサーバーは、加工したデータをブラウザに向けてレスポンスとして返す。
- レスポンスを受け取ったブラウザにて、JavaScriptが受け取った生データをより良いUXにするように加工する。
- JS/TSが加工したデータを基に、ブラウザが描画・表示する。
それでは各テストのカバー範囲を確認してみましょう。まずはわかりやすいのはE2Eのカバー範囲です。これはEnd-to-Endの名前の通り端から端まで、つまり1~7まですべての手順を網羅します。図だと青のシーケンスの箇所ですね。今回は説明の都合上バックエンド側は簡略化して説明しましたが、実際はユーザーが意識していないだけで様々なインフラサービスが使われています。E2Eはそれらを全てひっくるめてテストするので、ユーザーとほぼ同等の動きをするわけです。
APIテストの範囲はどこになるでしょうか?バックエンドサーバーの範囲になるので2~5になりますね。つまり図だと緑のシーケンスの箇所です。APIテストでは外部の別サーバーからバックエンドサーバーに直接HTTPSリクエストを行い、そのレスポンスを検証します。ブラウザ上(JS/TS)の挙動時間と最も物理的距離が遠いブラウザとバックエンドサーバーの通信時間を短縮することで、検証したいシステムの範囲を広げつつテスト時間もE2Eより短縮するという良いとこ取りしたのがAPIテストです。
最後にユニットテストのカバー範囲を確認しましょう。図だとオレンジのシーケンスの箇所で、バックエンドサーバーだと3と4に、クライアントサイドだと6に該当しますね。ですがそれだけでしょうか?私が指摘したいのは、クライアントサイドとバックエンドサーバーの長い方のシーケンスの箇所ですが、これらはユニットテストの範囲ではないのでしょうか?実はこの範囲もユニットテストで(技術的には)実現可能なのです。「何を言ってるんだ!2~5の範囲はAPIテストと言ったばかりじゃないか!」という批判が飛んでくるかと思いますが、それは次に説明します。
ユニットテストのより効果的な範囲
テスト対象システムの理解を深める
ユニットテストについて理解を深めるにあたり、皆さんはテスト対象システムがどのように作られて実行されているか知っているでしょうか?テスト対象システムがどのように動き、どのようなルールで開発者によって作られているかを知らなければ、ユニットテストのことを理解することは出来ません。この節では、この後のユニットテストの話に関係してくる、WebAPIサーバーの基本的な仕組みと、プログラミングの前提知識を確認しましょう。
WebAPIサーバーを起動するには、Webフレームワークを利用したプログラムをビルドすることで実現できます。そのWebフレームワークでは、URL1つに対して実行されるメソッド(サーバーに処理させる手順のこと)が1つ必ず紐づけています。クライアントサイドでリンクをクリックした時に期待する動作をするのは、まさにこの紐づけがしっかりなされているからです。リクエストを受け取るこのクラスをコントローラー(Controller)もしくはハンドラー(Handler)と呼びます。
さて、URLに紐づけられたこのコントローラーに全ての処理を書いても勿論動くのですが、可読性・再利用性の観点から全ての処理をそのメソッドに直接書くことはしません(逆に様々な処理を書いてしまったコントローラーを、俗にファットコントローラー(Fat Controller)といいます)。コントローラーのメソッドAに処理を直接で書いてしまうと、そこで使われている一部の処理を別のメソッドBで使い回したくても呼び出せないのです。そこでウェブアプリのドメイン毎にクラスを切リ分けて、コントローラーはそれを呼び出すことに専念します。このように、メソッドはまるでマトリョーシカのように入れ子構造となっていて、他のメソッドを呼ぶ"枝・幹"のようなメソッドと、他から呼ばれるだけの"葉"のようなメソッドがあるのです(以降何度かこれらの概念が出てくるので、本記事内では前者を枝幹メソッド、後者を葉メソッドと呼ぶことにします)。
本節はあくまで効果的なユニットテストを検討していくうえでの前提知識を説明した節なので、改めてこの節の内容をまとめておきましょう。本節で私が伝えたいことは、(1)WebAPIサーバーによって1つのURLで実行されるメソッド(コントローラーのメソッド)は1つだけと定められている、(2)実行されるコントローラーのメソッド内ではいくつも枝幹メソッドと葉メソッドが呼ばれる、以上2点です。
ユニットテストを技術的に作成可能な範囲
前節では、テスト対象システムには多くのメソッドが存在し、他のメソッドを呼び出す集約的な働きを担う枝幹メソッドもあれば、他のメソッドから呼び出されて特定の操作を行う葉メソッドもあることがわかりました。ユニットテストはこれらのどのメソッドをテストすることが出来るのでしょうか?
実は、枝幹・葉を問わず、呼び出し可能なメソッド全てに対してユニットテストを書くことが技術的には可能です。恐らく後述するテストサイズの話が基だと思いますが、ユニットテストを書くことが出来るのは葉メソッドだけだと勘違いしている方が偶にいらっしゃいます。しかし枝幹メソッドもまたユニットテストを書くこと自体は可能であり、例えばコントローラーのメソッドもまたユニットテストを書くことは出来るのです(cf: 先のシーケンス図の3~5のテストがユニットテストでも書けるという話)。コントローラーのメソッドのユニットテストはほぼAPIテストに近いものであり、違いといえば命令の方式がネットワークを経由したREST APIかローカル内のxUnitのコマンドの違いくらいでしょう。勿論、認証やルーティングなどAPIテスト独自の観点もあるにはありますが、挙動の確認という観点では同じです。
ユニットテストの作成を避けるべき範囲
技術的に全メソッドをユニットテストに書くことが出来るならば、全てのメソッドを動かしてテストしたほうが良いのでしょうか?答えは否で、あくまでユニットテストの実装は Googleが15年前から提唱しているテストサイズ に従った方が良いでしょう。DBとのやり取りやファイルアクセスといった、プログラム単体で完結しないメソッドは、ユニットテストとして実装しないほうが良いとされています。まして外部APIへのアクセスを含むメソッドは以ての外です。単一マシン・単一プロセスで完結しない処理は、どうしても速度が遅くなり開発者へのフィードバックが遅くなりますし、冪等性を担保できずにフレーキーな結果を招きやすくなるため、避けたほうが良いです。
では単一プロセスでは完結しない枝幹メソッドはテストしないのかと言うと、そうでもありません。例えばモック(より正確にはテストダブル)を使って、実際はアクセスしないけどアクセスして期待するデータを得た(或いは得られなかった)と仮定した上で、後続処理がどう動くかをテストするという方法があります。また依存性の注入(Dependency Injection)と呼ばれる設計を用いて、よりテストしやすい形に作り変えることで、プログラムの信頼性を担保するという方法もあります。一方でモックには欠点もあり、偽陰性(本当は失敗して欲しいテストが失敗しないケース)の基となるから多用は避けた方が良いと言われています。私もそれは同意ですが、あくまで多用乱用が駄目なだけで、用法用量を守って正しく使えばテストサイズを小さい方に持っていけるとても強力なツールだということも強く主張したいです。
(より詳しくはこちらを参照 → 第6回 自動テストのサイズダウン戦略(サバンナ便り) | Gihyo.jp )
ユニットテストがそこまで効果的高くない範囲、効果が高い範囲
ここまで、ユニットテストを書ける範囲と書かないほうが良い範囲を述べてきました。次に気になるのは、書いて効果が高い範囲と低い範囲です。
「これまで書いてこなかったユニットテストをちゃんと書くようにチャレンジしてみたけど、イマイチ効果が感じられない」という声を私はいくつか聞いたことがあります。これには原因が複数あり、例えばテストケースが悪い(正常系のみテスト、または同ケースばかりパターンを変えてテストしている等)とか、assertionを行わずテストとして意味をなしていないとかが挙げられますが、それほど効果が高くない範囲でユニットテストを多く実装しているというパターンもあると私は思っています。
そもそもユニットテストを書く意義は何かと言うと、刻一刻と変わるビジネス要件に追従するという目的のために、既存のコードへの修正を安心安全かつ高速に実行し続けることです。既存のコードに対して手を加えた時に、運悪くバグが混入してしまいリグレッションしてしまっても、それと同時に既存のテストが失敗することで、顧客にバグが届くことを防げたという安心を得られるわけです。これを言い換えると、あまり修正が入らないような箇所は、ユニットテストを記述する効果が他よりも薄いと言えるでしょう。
勘違いしてほしくないのが、「ユニットテストを書く効果が高くない」というのは、「ユニットテストを書く意味がない」とは違うということです。あくまで相対的な話であり、今修正が入らないからと言って未来永劫修正が入らないわけではなく、そういう意味ではユニットテストを書いておく意義は十分にあります。とはいえ、これまでユニットテストを書いてこなかったシステムだとするとそもそもテストコードが書きにくい設計となっていることは明白であり、テストコードが書きやすいのは修正が入りにくいutilの名付けられるようなメソッドであることが多いです。こういったコードに対してユニットテストを量産しても、変更が入らない以上いつまでもテストによる警告が発生しないので、効果が感じられないのは当然です。警報は不快ですが、鳴るからこそ意味があるのです。
では効果的なユニットテストとは何かといえば、ビジネスロジックを実装した箇所です。この箇所はどうしても肥大化しがちですが、テスト容易性を心がけたクラス設計・メソッド設計を行うことで、テストサイズを下げてユニットテストとして実装することが出来ます。E2EテストやAPIテストとして実装してしまうとフィードバックに時間がかかってしまい、結果的に開発工数が嵩むという悪循環を避けられるでしょう。残念ながら既存の実装箇所に対して後からユニットテストを書くのは至難の業でありアンチパターンですので、新規実装の際は必ずユニットテストを書くように心がけたいですね。
テストピラミッドにおけるユニットテストのグラデーション
テストピラミッドの3階層の最底辺を支えるのがユニットテストですが、先に述べたようにユニットテストとして書けるけどサイズ的に避けた方が良いものや、効果的なもの、効果が薄いものなど、様々なグラデーションがあると言えます。私は、ユニットテストの階層も更に以下の図のように階層化出来るのではないかと思っています。
一番下の階層は、主に葉メソッドのテストで、これらはそこまで変更頻度が高くなく、そこまで重要なロジックも持っていないようなメソッド群に対するテストです(私はプリミティブ層と名付けてみました)。一番上の階層は枝幹メソッドの中でも特にコントローラー層のメソッドで、これはAPIテストとの境界にあるようなテストであり、テストできる範囲が広くなるという他のメリットを持つAPIテスト側に任せてしまっても良く、薄めに作るのが良さそうです。重要なのは中間層で、枝幹メソッドの中でも変化が激しいビジネスロジックを実装した部分のテストこそがこのユニットテスト層の根幹となります。あくまでイメージではありますが、これくらい比率を分けて中間層を厚くしていくのが良いと私は思っています。
勘の良い方は「それってテストトロフィーと同じじゃね?」と気付いたかと思います。まさにその通りで、最後にテストピラミッドとテストトロフィーの関係性について未だに存在する誤解について紐解いていこうと思います。
テストピラミッドとテストトロフィー
テストトロフィーの概念は、ドッズによって2019年7月13日に発表されましたが( Write tests. Not too many. Mostly integration. )、その後の展開はあまり知られておらず、残念ながら今日に至るまでイメージ先行で広まっている部分が否定できません(恥ずかしながら私自身も、最近勉強するまでイメージでしか捉えてなかった一人です)。他の方々も書かれているとおり(ex: 第5回 テストピラミッド(サバンナ便り) | gihyo.jp )、インテグレーション層を厚くしようというテストトロフィーの考えは、用語の定義が異なるだけで結果的にはテストピラミッドに収束していくと言われています。しかしながら、どのように用語の定義が異なり、どうしてテストピラミッドに収束するかを説明した記事は少ないです。そこで本記事の最後の話題として、テストトロフィーへの反論とそれに対する再主張の流れを追ってみたいと思います。
テストトロフィーで用いられている用語の定義が異なると主張した著名人はファウラーで、ドッズの発表から約2年後の2021年6月2日にブログ記事を出しています( On the Diverse And Fantastical Shapes of Testing )。この中で、インテグレーションという言葉の曖昧さを指摘し、本記事でも語ってきた枝幹メソッドのテスト(ファウラーはSociable Testsと名付けている)のことをテストトロフィーではインテグレーションテストと呼んでいて、それはテストピラミッドで語っているインテグレーションテスト(結合テスト)とは違うものだと述べています。余談ですが、JSTQBでは定義されていない開発者間で使われる俗語である「内部結合テスト」という用語と非常に近いと私は思っています。
この反論を受けて、ドッズは翌日の2021年6月3日に2つの記事を投稿しました。一つは The Testing Trophy and Testing Classifications で、もう一つは Static vs Unit vs Integration vs E2E Testing for Frontend Apps です。前者ではファウラーから指摘されたインテグレーションの用語を再定義し、後者ではわざわざタイトルに for Frontend Apps を付けて主にバックエンドで使われていたテストピラミッドとは違うことを主張し直したわけです。
さて、未だ存在する誤解とは何かというと、インテグレーション層を厚くするというドッズの主張は、結合テスト(特にWebAPIテスト)を厚くすることとは違うということです。ドッズはreact-testing-libraryというフロントエンド用xUnitの支援ツールの開発者であり、バリバリのフロントエンドエンジニアであることから考えても、WebAPIテストを推進したいのではなく、Reactで実装された各コンポーネントの組み合わせの挙動のテストにこそ興味関心があったわけです。それはJest, Vitestといったフロントエンド用xUnitで実装されるものであり、だからこそユニットテストの拡充というテストピラミッドの考えに収束していくと言われているわけです。
以上が、テストピラミッドとテストトロフィーの関係性のまとめです。ユニットテストは規模を縮小させて良いものではなく、むしろSociable Testをどうユニットテストに組み込むかというサイズダウンの話であることがわかったかと思います。最近ではバックエンドとフロントエンドで異なるテスト目的を持つことで一つのテスト戦略とする考え方(ex: バクラク事業部のテストピラミッド設計 | LayerX Engineer Blog )も出てきていますが、これまでの流れを踏まえれば決して荒唐無稽な戦略ではなく、各システムの役目にあったテスト戦略であると言えるかと思います。
終わりに
本記事では、ユニットテストの適用範囲を様々な角度から考察してきました。ユニットテストの重要性に気付いた開発者が多くなってきたことで、もはやユニットテストを書くか書かないかの議論は終わり、どう書くかどう保守するかの議論がなされています。そのような中で、どうやって漏れなくテストするか、記述したテストをどう管理していくかについて開発者が悩み始め、QAの知見を求めて始めています。QAの皆さんが、「テスト自動化は私の守備範囲外」と食わず嫌いをせず、ユニットテストに歩み寄れるきっかけに本記事がなれたら幸いです。


