【ITニュース解説】Your-Tests-Are-Slow-and-Brittle-Youre-Testing-the-Wrong-Thing
2025年10月02日に「Dev.to」が公開したITニュース「Your-Tests-Are-Slow-and-Brittle-Youre-Testing-the-Wrong-Thing」について初心者にもわかりやすく解説しています。
ITニュース概要
テストが遅く頻繁に壊れるのは、UIやHTTPによるテストに頼りすぎているからだ。ビジネスロジックを外部要素から分離し、モックを使った高速で正確なユニットテストを重点的に行うべき。テストしやすいアーキテクチャが、質の高いソフトウェア開発の鍵となる。
ITニュース解説
ソフトウェア開発において、「もっとテストを書くべきだ」という言葉は、しばしば会議で繰り返される重要な考え方だ。しかし、多くの開発現場では、テストを書くことは苦痛を伴う作業となっている。その理由は、テストの実行が非常に遅く、まるでコーヒーを淹れる間に何杯も飲めるほど時間がかかったり、少しの変更で大量のテストが壊れてしまったりするからだ。例えば、Webページの見た目を制御するCSSのクラス名を変更したり、APIから返されるデータに新しい項目を追加したりするだけで、これまで問題なく動いていたはずのテストが一斉に失敗してしまうことがある。このような状況に陥る原因は、実はテストの書き方そのものだけでなく、アプリケーション全体の設計(アーキテクチャ)にある場合が多い。良い設計のフレームワークは、初めからテストしやすい構造へと開発者を導いてくれる。
多くの開発者、特に経験の浅い人たちは、「テスト」と聞くと「ユーザーがソフトウェアを使う様子をシミュレーションすること」だと考える傾向がある。そのため、以下のような方法で大量のテストを書くことに注力してしまう。まず、完全に動作するWebサーバーを起動し、バックエンドのテストであれば、実際のHTTPリクエストを送信して応答を確認する。フロントエンドのテストであれば、Webブラウザを起動し、特定のボタンを見つけてクリックし、ページの表示内容や返されるHTTPステータスコード、JSONデータが期待通りかを検証する。
これらのテストは「エンドツーエンドテスト」や「結合テスト」と呼ばれ、システム全体が正しく連携して動くことを確認するために非常に有用だ。しかし、もしあなたのテスト戦略がこの種類だけに頼っているなら、それは大きな間違いを犯している可能性がある。なぜなら、これらのテストにはいくつかの深刻な問題があるからだ。
第一に、非常に実行が遅いという問題がある。サーバーの起動、ネットワーク接続の確立、データの送受信といった各ステップに時間がかかり、一つのテストで数十ミリ秒、あるいは数百ミリ秒かかることも珍しくない。何百、何千というテストがある場合、全体の実行時間は数分、時にはそれ以上にもなり、開発者がフィードバックを得るまでの時間が大幅に延びてしまう。
第二に、壊れやすいという問題がある。これらのテストは、ユーザーインターフェースの構造やAPIのデータ形式といった外部の細部に強く依存している。そのため、APIの応答に新しいフィールドが追加されたり、画面の要素の配置が変わったりするだけで、テストが失敗してしまうことがある。これらのテストは「見せ方」に注目しすぎているため、「内部のロジック」の変化には弱く、意図しないテストの失敗を引き起こしやすい。
第三に、システムの端にある特別なケース(エッジケース)を網羅するのが難しいという問題がある。例えば、「ユーザーが作成された直後にデータベースが停止し、ウェルカムメールが送信される前にエラーが発生した場合、どうなるか?」といった複雑なビジネスロジックの分岐や例外条件を、HTTPリクエストを通じて正確にシミュレートするのはほとんど不可能だ。テストのために実際にデータベースサーバーの電源を抜くわけにはいかないだろう。
では、どのようにテストすれば良いのだろうか。適切なテスト戦略は、「テストピラミッド」の形をしているべきだと言われている。ピラミッドの底辺には、大量の高速で信頼性の高い「単体テスト」を配置する。その上の中央部分には、単体テストより数は少ないが、いくつかのコンポーネントが連携する部分を確認する「結合テスト」を配置する。そして、ピラミッドの頂点には、システム全体がユーザーにとって正しく機能するかを確認する、ごく少数の「エンドツーエンドテスト」を配置する。
このテストピラミッドを実現するための鍵は、アプリケーションを層(レイヤー)に分けて設計することにある。適切に設計されたアプリケーションでは、最も重要で複雑なビジネスロジックは、Webフレームワークやデータベースといった外部の要素から完全に切り離されているべきだ。
例えば、ユーザー登録のロジックを扱うUserServiceというサービスを考えてみよう。このサービスは、ユーザー名が既に存在するかを確認し、存在しなければ新しいユーザーを保存するというコアな処理を担当する。このようなロジックは、Webサーバーの起動や実際のデータベースへの接続を必要としない「純粋な」コードとして書かれている。
このUserServiceのregister_user関数をテストする場合、私たちはWebサーバーを起動したり、実際のデータベースに接続したりする必要はない。ただ、そのロジック自体をテストすればよい。データベースの役割は「テストダブル」、特によく使われる「モックオブジェクト」という代替品を使って置き換える。モックオブジェクトは、実際のデータベースのように振る舞うが、実際にはメモリ上で動作し、テストのためにあらかじめ設定された値を返すように作られた、テスト専用のオブジェクトだ。
具体的には、UserServiceが依存するUserRepository(ユーザーデータを保存・取得する役割)のモックを作成する。このモックに対して、「find_by_usernameというメソッドが特定のユーザー名で呼び出されたら、既にユーザーが存在するという結果を返す」といった振る舞いを設定する。そして、このモックオブジェクトをUserServiceに渡してregister_user関数を呼び出す。すると、UserServiceはモックオブジェクトが返す結果に基づいてロジックを実行し、ユーザー名が既に存在する場合はエラーを返すという結果になる。私たちはこの結果を検証することで、「ユーザー名が既に存在する場合、登録は失敗する」というビジネスロジックが正しく実装されていることを確認できる。
このような単体テストには、いくつかの素晴らしい利点がある。 第一に、非常に高速だ。テストはメモリ内で実行され、データベースへのアクセスのような入出力処理が一切発生しないため、数ミリ秒以下で完了する。これにより、何千ものテストを数秒で実行し、開発者はすぐにフィードバックを得ることができる。
第二に、非常に正確だ。このテストは、「ユーザー名が存在したら登録が失敗する」という、私たちが本当に気にしているビジネスロジックだけをテストする。外部のネットワーク状況やデータベースの状態などに影響されることなく、特定のロジックの正しさを保証できる。
第三に、非常に強力だ。モックオブジェクトを使うことで、さまざまな特殊なケースを簡単にシミュレートできる。例えば、「データベースへの接続が失敗したらどうなるか?」といったシナリオも、モックに「保存処理が呼び出されたら、接続失敗のエラーを返す」と設定するだけでテストできる。このような高度な制御は、エンドツーエンドテストでは実現が難しいものだ。
もちろん、アプリケーションには「コントローラー」と呼ばれる、Webリクエストを受け取ってサービス層に処理を依頼し、その結果をWeb応答として返す部分も存在する。しかし、複雑なビジネスロジックはサービス層で既に単体テストによって網羅されているため、コントローラー層のテストは、ルーティングが正しくサービス層のメソッドに紐づいているか、JSONデータの送受信が期待通りに機能するかといった、非常にシンプルな「ハッピーパス」(正常な処理経路)のテストを数多く書けば十分だ。
このように、アプリケーションのテストのしやすさは、その設計(アーキテクチャ)に大きく依存する。テストが苦痛なのは、テストそのものが悪いのではなく、コアなビジネスロジックを独立して高速に単体テストできないような、複雑に絡み合った「ビッグボールオブマッド」のようなアーキテクチャになっているからだ。良いWebフレームワークは、その設計思想や提供するプロジェクトのひな形を通じて、開発者がコアロジックをWeb層から切り離し、依存性注入(必要な部品を外部から渡す仕組み)やインターフェース(特定の振る舞いを定義する型)を活用するように促す。これにより、開発者は、稲妻のように速く、堅牢な単体テストに多くの時間と労力を費やせるようになる。テストがもはや負担ではなく、迅速で信頼できるフィードバックの道具となれば、開発者は心からテストを好きになるだろう。
だから、次に新しいフレームワークを評価する際には、「使いやすそうか?」と問うだけでなく、「このフレームワークで書かれたコードは、簡単にテストできるか?」という問いも加えてみてほしい。なぜなら、良いテストを簡単に書けるフレームワークこそが、最終的に高品質で信頼性の高いアプリケーションを構築する上で、あなたを本当に助けてくれるからだ。