SOLID原則 (テストコードの品質向上)
テストコードの保守性・可読性を高めるための原則です。
上記の記事で触れている、
良い単体テストの特性であるFIRST原則
を達成するために、SOLIDの思想は不可欠なので、是非ともQAの方々は常識にしてしまうことをお勧めします。
S:単一責任の原則
概要
一つのテストクラスやテストメソッドは、一つのことだけを検証する責任を持つべきです。
複数のことを検証できる多目的なテストクラスやメソッドは、その保守がしにくい原因となります。
メカニズム
テストが失敗した際、その原因が即座に特定できるようにします。
「失敗する理由が一つだけ」の状態を目指します。
つまり、
「正常系のログイン」をテストするメソッドと、「不正なパスワードでのログイン」を
テストするメソッドは分けるべきです。
メソッド単位だけでなく、同様にしてテストクラスやテストコンポーネントも、
下図のように「唯一1つのものだけをテストする」という状態にしましょう。
間違っても、✖の方みたいに、あれもこれもテストするなんて状態はダメです。
具体例
TestUserServiceクラスにおいて、
・「有効な情報でのユーザー作成」をテストするtest_create_user_succeedsメソッドと、
・「重複メルアドでのユーザー作成」をテストするtest_create_user_fails_on_duplicate_email
は、別々のテストメソッドとして定義します。
O:オープン・クローズドの原則
概要
テストスイートは、既存のテストコードを修正することなく、新しいテストケースを追加(拡張)できるように設計すべきです。
仮に既存のどこかを修正する必要があるとしても、上記の 「単一責任の原則」 を満たし、
素早く修正箇所や影響範囲を特定できるように設計されているべきです。
メカニズム
データ駆動テストなどの手法を用い、テストロジックとテストデータを分離します。
新しいテストケースは、テストデータを追加するだけで実現できます。
データ駆動型のテストは、正常なデータ、異常なデータ、境界値のデータなど、
1つのシナリオのプロセスは変えずに、入力値をいろいろ変えて出力結果をテストしたい場合に非常に役立ちます。
具体例
複数の入力パターンを検証する割引計算のテストを例にとります。
テストロジックは1つだけ書き、入力値(購入金額、会員ランク)と期待される結果(割引率)
をCSVやJSONファイルに定義します。
これにより新しいテストパターンは、そのファイルに行を追加するだけで済みます。
L:リスコフの置換原則
概要
基本となるテストクラスを、そのサブテストクラスに置き換えても、テスト全体が問題なく動作すべきです。
ある特定のサブタイプでのみ、テストの挙動がおかしくなるなんてことは許されません。
ちなみに、この原則が守られていないと、
自ずと上記のオープン・クローズドの原則も満たせなくなります。
契約による設計と密接に関わっています。
上位の型における、すべてのメソッドの事前・事後・不変条件の特性を
下位の型継承したサブタイプでは、引き継がなくてはなりません。
メカニズム
テストコードで型継承やimplementsを使う場合に適用されます。
派生クラスは、基底クラスのセットアップや振る舞いを壊さないように実装します。
型継承しているわけですから、当然上位の型特性を全て引き継ぐ必要があります。
例えば、BaseApiTestCaseを継承したUserApiTestCaseは、テストランナーから見ればBaseApiTestCaseとして扱えても問題ないように設計します。
具体例
DB接続とクリーンアップを行うBaseDatabaseTestクラスを作成します。
UserApiTestとProductApiTestがこれを継承する場合、どちらも基底クラスのDB管理機能を壊さずに、自身のテストを実行できなければなりません。
I:インターフェース分離の原則
概要
テストは、そのテストに必要のない機能(セットアップやヘルパー)に依存すべきではありません。
その必要のない機能が仮に変更された際に、本来であれば受けなくていい影響を受けるリスクがあるからです。
この原則を守ると、外部のクライアントに提供する際に、一部は使われない、
クライアント側のテストコンポーネントからして、「どれは使ったらいけないのか」を意識しなくていい設計となります。
メカニズム
一つの巨大なsetup_everything()ヘルパーを作るのではなく、小さく、目的に特化したヘルパー関数やフィクスチャを複数用意します。
また、それ以外にもテストコンポーネントの提供するAPIへの設計でも適用されます。
詳細は、以下の具体例で触れます。
具体例① -使わないヘルパー関数に依存させない-
パスワードリセットメールの送信機能をテストする場合、create_user()というヘルパーだけを使います。
「注文」や「商品」といった、このテストに無関係なデータまで作成する巨大なprepare_test_data()ヘルパーを呼び出すべきではありません。
具体例② -テストAPIの多目的解消-
たとえば、下図において、クライアントコンポーネントに相当するYの方が、
テストモジュールAに関する公開ロジックしか使わないとします。
この場合、
テストAPIのロジック中で、テストモジュールBに関するロジックが変更されたら、
当然その影響をテストコンポーネントYは受けることになります。
全く使っていないにも関わらずです。
このような場合には、以下のように
使う処理のみがまとまったテストAPIになるように、分割しましょう。
(インターフェイスレベルの単一責任原則と表現したらわかり良いかもです)
D:依存性逆転の原則
概要
テストは、プロダクションコードの具象実装ではなく、抽象(インターフェース)に依存すべきです。
この設計の目的は、テスト対象(プロダクションコード)を、それが依存する
他のコンポーネントから完全に切り離す(分離する) ためです。
テストの分離性、安定性、保守性を劇的に向上させるための重要なプラクティスです。
メカニズム
テスト対象が依存している外部コンポーネント(DB、APIなど)を、インターフェースを介してモックやスタブに差し替えます。
これにより、テスト対象を完全に分離し、高速で安定した単体テストを実現します。
プロダクションコードは、多くの場合、上図のようにデータベース、外部API、ファイルシステムといった他のコンポーネントやモジュールに依存しています。
もしテストが具象なプロダクションコードのクラスに直接依存すると、そのテストは
依存先のコンポーネントもすべて含んだ状態で実行しなくてはならない
という状態を招きます。(※赤枠全てをテストしないといけなくなる)
これは、そもそも単体テストが、粒度が肥大化し、結合テスト粒度になるということです。
具体例
OrderServiceをテストする際、IOrderRepositoryというインターフェースに依存させます。
テストコードでは、このインターフェースを実装したInMemoryOrderRepositoryというテスト用の偽オブジェクトを注入し、本物のデータベースなしでテストを実行します。