前置き
前回書いた以下の記事をより深ぼってみましょう。
最も一般的で分かりやすい単一責任原則(SRP)の適用方法は、
1つのテストクラス(またはテストファイル)が、1つのプロダクションコードのクラス(またはモジュール)に責任を持つという、1:1の対応関係です。
なぜ1対1が基本なのか?
この対応関係は、テストの凝集度と発見性を最大化するための、非常に実践的なプラクティスです。
凝集度 (Cohesion)
プロダクションコードのOrderServiceクラスに変更を加えた際、修正が必要になる可能性のあるテストは、OrderServiceTestクラス(またはOrderServiceTest.javaファイル)にすべてまとまっています。
これにより、
変更の影響範囲が明確になり、修正が容易になります。
※これはテストコンポーネント凝集の原則(特にCCP:閉鎖性共通の原則)にも通じます。
発見性 (Discoverability)
開発者は、OrderServiceのテストを探すときに、迷わずOrderServiceTestを探しに行けます。
この予測可能性は、チーム開発において認知的な負荷を下げ、生産性を向上させます。
具体的な対応例
src/main/java/com/example/
OrderService.java // OrderServiceクラスを定義
src/test/java/com/example/
OrderServiceTest.java // OrderServiceTestクラスを定義
この構造では、OrderServiceTestの責任は「OrderServiceの振る舞いを検証すること」
そのただ一つです。
より深いレベルでのSRP
単一責任原則はテストクラスだけでなく、テストメソッドの単位にも適用されます。
単一責任原則(SRP)をテストクラスからメソッドというさらにミクロな視点へと適用した場合の話です。
「便利だから~」という単純な理由で、1つのテストクラスが複数のことをテストする設計にすると、そのモジュールのコンテキストを見失うことにも繋がり、テスト自体の保守性が低下します。
テストクラスの責任
1つのプロダクションクラスの振る舞いを検証する。
これがSRPを満たすためには、
その内部のテストメソッドが単一責務を満たすようにすること
テストメソッドの責任
1つのプロダクションメソッドの、さらに特定の1つの振る舞い(シナリオ) を検証する。
この時に、是非ともアクティビティ図を描いてみてください。
途中で条件分岐、バリエーションが発生するようなテストシナリオになるのなら、より具体なシナリオに分けましょう。
分けてからそれらをグルーピングしましょう。
具体例
OrderServiceクラスにplaceOrderというメソッドがあるとします。
OrderServiceTestクラスの中は以下のようになります。
こんな感じに、各テストメソッドが 「失敗する理由が1つしかない」状態 になっているのが、SRPが適用された良いテストの証です。
よりマクロへの適用
では、単一責任原則(SRP)をテストクラスやメソッドというミクロな視点から、
テストモジュールやコンポーネントというマクロな視点へと引き上げて設計するためのメカニズムを解説します。
ここで中心的な役割を果たすのが、コンポーネント凝集の原則(CCP, REP, CRP) です。
これらは、関連するテストクラス群をどのように「意味のある単位」としてまとめ、管理すべきかを教えてくれます。
🧠 なぜテストコードにコンポーネント原則を適用するのか?
プロダクションコードが成長するにつれて、テストコードもまた、それ自体が巨大で複雑なソフトウェアになります。
この「テストというソフトウェア」を無秩序なまま放置すると、保守不能な技術的負債の塊になります。
プロダクションコードの品質を担保するためのテストコードが、技術的負債になるとか、テスト工程がデリバリーのボトルネックになって、本当に最悪です。
コンポーネント原則を適用する目的は、
テストスイート全体を、変更に強く、理解しやすく、再利用可能な部品の集合体として設計すること
です。SRPが個々のテスト部品の品質を保証するなら、コンポーネント原則は部品同士の適切な「梱包」と「関係性」を定義します。
1. 📦 閉鎖性共通の原則 (CCP: The Common Closure Principle)
概要
「同じ理由・同じタイミングで変更されるコンポーネントは、1つにまとめるべき」
という原則です。
テスト設計においては、「同じプロダクションコードの変更によって影響を受けるテストは、すべて同じテストモジュール内に配置すべき」 という意味になります。
メカニズム
この原則は、変更の影響範囲を局所化するためのものです。
プロダクションコードのある機能(例:決済サービス)に仕様変更が入った場合、
開発者は迷わず対応するテストモジュール(例:payment_tests)だけを見れば、
1:1対応した修正すべきテストがすべてそこにある
という状態を目指します。
このテストモジュールが、変更に対する「防波堤」の役割を果たします。
適用しないことによるデメリット
決済サービスの変更が、ユーザー認証のテストや商品カタログのテストなど、
無関係に見える場所にまで影響を及ぼし、テストの失敗が広範囲に飛び火します。
これにより、リファクタリングへの恐怖心が生まれ、開発の俊敏性が著しく低下します。
具体例
あるマイクロサービスアーキテクチャにおいて、Order Service(注文サービス)に関連するテストは、すべてcom.example.tests.services.orderserviceという単一のJavaパッケージ(テストモジュール)に集約します。
このパッケージ内にはOrderCreationTest.java、OrderCancellationTest.javaなどが含まれます。
注文サービスの仕様変更で影響を受けるのは、原則としてこのパッケージ内だけ
になっていることが、望ましい形です。
そのパッケージ内以外にもあるってことは、影響範囲が閉じていない。
つまり、オープン・クローズドに反している状態です。
2. 🔖 再利用・リリース等価の原則 (REP: The Reuse/Release Equivalence Principle)
概要
「再利用の単位とリリースの単位は等価になるべき」
という原則です。
テスト設計においては、「複数のチームやプロジェクトで共有されるテスト用のヘルパーやユーティリティは、独立したライブラリとしてバージョン管理され、リリースされるべき」 という意味になります。
メカニズム
この原則は、再利用されるコンポーネントに安定性と予測可能性をもたらすためのもの。
共有テストライブラリの利用者は、v1.2.0のようにバージョンを明示的に指定することで、
ライブラリの予期せぬ変更から自分たちのテストスイートを保護できます。
ライブラリの更新は、利用者が自身のタイミングで意図的に行うことができます。
適用しないことによるデメリット
バージョン管理されていない共有テストコードは、
誰か一人の変更が、それを再利用している全てのチームのテストを突然破壊する
原因となります。
これにより、共有コンポーネントへの信頼が失われ、各チームが同じようなヘルパーを再発明する「テストコンポーネントのサイロ化」が進みます。
チームトポロジー的に言うとすると、QAチームの知識サイロ化が起きます。
具体例
複数のマイクロサービスで、テスト用の認証トークン(JWT)を生成する必要があるとします。
このロジックをcompany-auth-test-helpers
という独立したライブラリとして作成し、
社内のパッケージリポジトリ(Maven CentralやArtifactory)にv1.0.0
として公開します。
各サービスのテストは、pom.xml
などでこのライブラリの特定のバージョンに依存します。
3. 🧩 全再利用の原則 (CRP: The Common Reuse Principle)
概要
「コンポーネントの利用者は、自身が利用しないものに依存すべきではない」
という原則です。
テスト設計においては、「テストユーティリティモジュールは、小さく、目的に特化しているべき」 という意味になります。
メカニズム
この原則は、不必要な依存関係を排除するためのものです。
例えば、DB操作のテストを書きたいだけなのに、「巨大な共通ヘルパーをインポートした結果、APIクライアントやUIテスト用のライブラリまで依存関係に含まれてしまう」 といった事態を避けます。
モジュールを小さく保つことで、各テストが必要とする依存関係を最小限に抑えます。
適用しないことによるデメリット
巨大で多機能な「神テストユーティリティモジュール」が生まれます。
このモジュールは、少しの変更でも影響範囲が非常に大きくなるため、誰もが修正を恐れるようになります。
また、無関係なライブラリへの依存は、CI/CD環境のセットアップを複雑にし、テストの実行時間を不必要に長くします。
具体例
アンチパターン
TestUtils.javaというクラスに、DBヘルパー、APIモック、データ生成、カスタムアサーションなど、考えうる全てのユーティリティを静的メソッドとして詰め込む。
良い例:責務に応じてファイルを分割する
・DbTestHelpers.java (DBのセットアップとクリーンアップ)
・ApiMockHelpers.java (外部APIのモックサーバー)
・TestDataFactories.java (テストデータ生成)
DBのテストを書く際は、DbTestHelpers.javaだけをインポートすれば済みます。