【ITニュース解説】Your-Tests-Are-Slow-and-Brittle-Youre-Testing-the-Wrong-Thing
2025年10月04日に「Dev.to」が公開したITニュース「Your-Tests-Are-Slow-and-Brittle-Youre-Testing-the-Wrong-Thing」について初心者にもわかりやすく解説しています。
ITニュース概要
遅く脆いテストは、UI/HTTP経由の結合テストに偏るためである。テストピラミッドに従い、ビジネスロジックを分離して高速な単体テストを重視するテストしやすいアーキテクチャが、開発の質を高める鍵となる。
ITニュース解説
ソフトウェア開発において「もっとテストを書くべきだ」という言葉は、しばしば合言葉のように唱えられる。誰もがテストの重要性を理解しているが、実際にテストを書く段になると、多くの開発者が苦痛を感じているのが現状だ。なぜなら、テストの実行が非常に遅く、数多くのテストを回すにはコーヒーを何杯も淹れる時間がかかるほどだ。テストコード自体も、本来のビジネスロジックよりも複雑で理解しにくい場合がある。さらに深刻な問題は、テストが「壊れやすい」ことだ。例えば、Webページの見た目を決めるCSSのクラス名を一つ変更したり、APIの応答データにフィールドを追加したりするだけで、何百ものテストが理由もわからず失敗することがある。もしこのような状況に心当たりがあるなら、それはテストそのものの問題ではなく、アプリケーションの設計(アーキテクチャ)がテストを非常に困難にしている可能性が高い。優れたフレームワークは、テストしやすいアーキテクチャの構築を開発者に促すものなのである。
多くの開発者、特に経験の浅い人たちは「テスト」を「ユーザーの行動をシミュレーションすること」だと解釈しがちである。そのため、Webサーバーを起動し、実際のHTTPリクエストを送信したり、ブラウザを立ち上げてボタンをクリックしたりするといった、自動化されたユーザー操作を多数のテストで実行しようとする。そして、返されるHTTPステータスコードやJSONの内容、表示されるテキストが期待通りかを確認する。Node.jsの世界で、Supertestのようなライブラリを使ってExpressアプリケーション全体をテストする例は、この考え方の典型である。このようなテストは「エンドツーエンドテスト」や「結合テスト」と呼ばれる。これらが役に立つのは当然で、システム全体が連携して正しく機能することを確認できる。しかし、テスト戦略をこの種のものだけに頼るのは大きな間違いである。
この手法には複数の問題点がある。第一に、非常に遅い。サーバーの起動、ネットワーク接続の確立、JSONデータの変換など、各ステップに時間がかかる。一つのテストが完了するまでに数十ミリ秒、あるいは数百ミリ秒を要することもある。数百、数千ものテストがある場合、合計で数分、あるいはそれ以上かかり、開発のフィードバックサイクルが著しく遅れてしまう。第二に、壊れやすいことだ。これらのテストは、UIの構造やAPIの契約といった外部の詳細に密接に結合しすぎている。APIの応答にフィールドが追加されただけでテストが失敗することもある。これらは「見た目」を気にしすぎるあまり、「内部のロジック」の変化に過敏に反応してしまうのだ。第三に、エッジケース(例外的な状況)をカバーするのが難しいという点がある。中核となるビジネスロジックには多くの分岐やエラー条件が含まれる。例えば、「ユーザーが作成された直後だが、ウェルカムメールが送信される前にデータベースがダウンしたらどうなるか?」といったシナリオを、HTTPリクエストだけで正確にシミュレートするのはほぼ不可能である。テストのために実際にデータベースサーバーの電源を抜くわけにはいかないだろう。
適切なテスト戦略は、ピラミッドのような構造をしているべきだとされる。ピラミッドの底辺には、大量の高速で信頼性の高い「ユニットテスト」が存在する。中間層には、より少ない数の「結合テスト」があり、そして頂点には、ごく少数の「エンドツーエンドテスト」が位置する。このピラミッドを実現するための鍵は、「レイヤードアーキテクチャ」と呼ばれる設計にある。適切に設計されたアプリケーションでは、中核となるビジネスロジックは、Webフレームワークやデータベースといった外部の世界から完全に分離されているべきである。例えばHyperlaneのような設計思想では、最も価値があり複雑なロジックを「サービス層」や「ドメイン層」に配置することを推奨している。これらの層は、Webの詳細とは独立した純粋なコードで構成されているため、最も純粋で高速なユニットテストの対象となる。
UserServiceという、ユーザー登録ロジックを扱うサービスを例に考えてみよう。このサービスは、具体的なデータベースの実装ではなく、抽象的なインターフェース(Rustではトレイト)に依存するように設計されている。register_user関数は、ユーザー名が既に存在するかをチェックし、存在しなければ新しいユーザーを作成して保存するといった中核ロジックを持つ。このregister_user関数をテストする際、実際のサーバーを起動したり、実際のデータベースに接続したりする必要はない。ただ、ロジックそのものをテストすればよい。ここで活躍するのが、「テストダブル」と呼ばれる、特に「モックオブジェクト」である。これは、UserServiceが依存するUserRepository(ユーザーデータを管理する部分)の代わりを務める偽物のオブジェクトである。
Rustでは、mockallのようなライブラリを使ってモックオブジェクトを簡単に作成できる。例えば、ユーザー名が既に存在する場合に登録が失敗するというシナリオをテストする際、モックのUserRepositoryが「find_by_usernameが『testuser』という引数で一度呼ばれたら、既存のユーザーを返す」ように設定できる。そして、このモックオブジェクトをUserServiceに注入し、register_user関数を呼び出す。最後に、結果が「UsernameExists」というエラーになることを確認する。
このテストコードにはいくつかの利点がある。まず、非常に高速である。メモリ内で実行され、ファイルの読み書きやネットワーク通信といった入出力(I/O)は一切発生しない。完了までにミリ秒単位の時間がかかるため、何千ものテストを数秒で実行し、迅速なフィードバックを得ることができる。次に、非常に正確である。「ユーザー名が存在すれば登録は失敗する」という、私たちが本当に気にしているビジネスロジックだけをテストする。外部要因に影響されることはない。そして、強力である。データベース接続が失敗した場合など、様々なエッジケースを簡単にシミュレートできる。モックのsave関数がエラーを返すように設定するだけで、本番環境で再現が難しい状況をテスト環境で自在に作り出すことが可能だ。このような制御レベルは、エンドツーエンドテストでは到底実現できない。
もちろん、コントローラー層(Webリクエストを受け取り、サービス層と連携する部分)については、いくつかの結合テストが必要である。これにより、ルーティングが正しくサービス層のメソッドと連携しているか、JSONの変換が期待通りに機能するかを確認する。しかし、複雑なロジックのほとんどはすでにサービス層のユニットテストでカバーされているため、コントローラーのテストは非常にシンプルな「ハッピーパス」(正常系)のテストで十分であり、ごく少数で足りる。
結論として、テストしやすいアプリケーションとテストしにくいアプリケーションの最大の違いは、そのアーキテクチャにある。テストそのものが苦痛なのではなく、独立した高速なユニットテストをビジネスロジックに対して実行できない「ビッグボールオブマッド」(整理されていない複雑な構造)なアーキテクチャが原因なのである。優れたWebフレームワークは、その設計思想やプロジェクトのひな形を通じて、最初からテストしやすい道へと開発者を導く。それは、中核ロジックをWeb層から分離し、依存性注入やインターフェース(トレイト)の使用を推奨する。これにより、開発者は、稲妻のように速く、堅牢なユニットテストに労力の90%を費やすことができるようになる。テストがもはや負担ではなく、迅速で信頼性の高いフィードバックツールになったとき、開発者は心からテストを好きになるだろう。
したがって、次にフレームワークを評価する際には、「使うのが楽しいか?」だけでなく、「それを使って書かれたコードはテストしやすいか?」という問いも投げかけるべきである。なぜなら、良いテストを簡単に書けるよう支援するフレームワークこそが、最終的に高品質で信頼性の高いアプリケーションを構築する上で真に役立つものだからである。