テスト可能なコードを書く

公開: 2022-11-03

単体テストは、ソフトウェア開発者のツールボックスに不可欠なツールです。 単体テストの作成は、ソフトウェア設計のベスト プラクティス、パターン、および原則に従っているコードベースを扱う場合、比較的簡単です。 実際の問題は、設計が不十分でテストできないコードを単体テストしようとしたときに発生します。

このブログでは、より単体テストしやすいコードを作成する方法と、テスト容易性を向上させるために避けるべきパターンと悪いプラクティスについて説明します。

テスト可能なコードとテストできないコード

長期的に保守可能である必要がある大規模なアプリケーションで作業する場合、システムの全体的な品質を高く保つために、自動テストに頼る必要があります。 複数のユニットをまとめてテストする統合テストと比較して、単体テストには高速で安定しているという利点があります。 理想的にはテスト対象のクラスだけをインスタンス化するため高速であり、通常はデータベースやネットワーク接続などの外部依存関係をモックアウトするため安定しています。

単体テストと統合テストの正確な違いに慣れていない場合は、このトピックの詳細について、テストの概要ブログを参照してください。

テスト可能なコードは、コードベースの残りの部分から分離できます。 つまり、最小単位を個別にテストできます。 テスト不可能なコードは、適切な単体テストを作成するのが困難または不可能な方法で作成されます。

テスト可能なコードを書くときに避けるべき、いくつかのアンチパターンと悪い習慣を確認しましょう。

例は Java で記述されていますが、ここで説明するコーディング規則は、オブジェクト指向プログラミング言語およびテスト フレームワークに適用されます。 このブログ投稿の例では、assertj と JUnit5 を使用します。

依存性注入

依存性注入は、テストの分離を実現するための最も重要な設計パターンの 1 つです。 依存性注入は、1 つのオブジェクトがコンストラクター パラメーターまたはセッターを介して他のオブジェクト (依存性) を受け取る設計パターンであり、それら自体を構築する必要はありません。

依存性注入を使用すると、オブジェクトの依存性をモックアウトすることで、テスト対象のクラスを簡単に分離できます。

依存性注入のない例を見てみましょう:

エンジンの依存関係は Car クラスのコンストラクターで構築されているため、Car クラスと Engine クラスは密結合であると言えます。 それらは相互に大きく依存しています。一方を変更すると、もう一方も変更する必要があります。

テストの観点からは、上記の例では具体的なエンジンの実装をテスト ダブルに置き換えることができないため、Car クラスを単独でテストすることはできません。

ただし、依存性注入とポリモーフィズムを使用して分離を実現できます。

これで、複数のエンジンの実装を構築できるため、異なるエンジンを搭載した車を構築できます。

Engine 抽象化のモック実装を作成し、それを Car クラスに渡すことができるため、分離したテストが可能になりました。

他のオブジェクト (依存関係) を必要とするオブジェクトを扱うときは、コンストラクター パラメーター (依存関係の挿入) を介してそれらを提供する必要があります。

このパターンに従うことで、コードが読みやすくなり、時間の経過に伴う変更に適応できるようになります。 また、コンストラクターで実際の作業を行うことは避けてください。フィールドの割り当て以外は実際の作業です。 コンストラクターの `new` キーワードは、常にテストできないコードの警告サインです。

ここで注意すべきことの 1 つは、状況によっては、密結合 (たとえば、コンストラクターの新しいキーワード、論理分離のための内部クラス、オブジェクト マッパー) は悪い習慣ではないということです。クラスは「スタンドアロン」クラスとしては意味がありません。

グローバル状態

グローバルな状態を共有すると、特にマルチスレッド環境で不安定なテスト (成功する場合もあれば失敗する場合もある) が生成されることがよくあります。

テスト対象の複数のオブジェクトが同じグローバル状態を共有するシナリオを想像してください。オブジェクトの 1 つのメソッドが、共有されたグローバル状態の値を変更する副作用をトリガーすると、他のオブジェクトのメソッドからの出力は予測不能になります。 何らかの方法でグローバル状態を変更したり、何らかのグローバル状態にプロキシされたりするため、不純な静的メソッドの使用は避けてください。

この不純な静的メソッドを見てみましょう。

基本的に、このメソッドは現在のシステムの日付と時刻を読み取り、その値に基づいて結果を返します。 LocalDateTime.now() 静的呼び出しは、テストの実行中に異なる結果を生成するため、このメソッドに対して適切な状態ベースの単体テストを作成することは非常に困難です。 システムの日付と時刻を変更しないと、このメソッドのテストを作成することはできません。

これを修正するには、日時を引数として timeOfDay メソッドに渡します。

timeOfDay 静的メソッドは純粋になりました。同じ入力は常に同じ結果を生成します。 これで、分離された dateTime オブジェクトをテストで引数として簡単に渡すことができます。

デメテルの法則

デメテルの法則、または最小知識の原則は、オブジェクトは最初のオブジェクトに密接に関連するオブジェクトについてのみ知っているべきであると述べています. つまり、1 つのオブジェクトは、必要なオブジェクトにのみアクセスできる必要があります。 たとえば、コンテキスト オブジェクトを引数として受け入れるメソッドがあります。

このメソッドは、その作業を行うために必要な情報を取得するためにオブジェクト グラフをたどる必要があるため、デメテルの法則に違反しています。 クラスやメソッドに不必要な情報を渡すと、テスト容易性が損なわれます。

他のオブジェクトへの参照を含む巨大な BillingContext オブジェクトを想像してください。

ご覧のとおり、テストは重要でない情報で肥大化しています。 複雑なオブジェクト グラフを作成するテストは読みにくく、不必要に複雑になります。

前の例を修正しましょう。

クラスとメソッドには、常に直接的な依存関係を渡す必要があります。 ただし、多くの引数をメソッドに渡すことも良い方法ではありません。理想的には、最大で 2 つの引数を渡すか、関連する引数をデータ オブジェクトにラップする必要があります。

神様のオブジェ

神のオブジェクトは、他の多くの異なるオブジェクトを参照し、複数の責任を持ち、変更する複数の理由を持つオブジェクトです。 クラスが行っていることを要約するのが難しい場合、または要約に「および」という言葉が含まれている場合、クラスには複数の責任がある可能性があります。

神のオブジェクトは、さまざまなレベルの抽象化と懸念が混在する複数の無関係な依存関係を扱っているため、テストが難しく、多くの副作用が発生します。 その結果、テストケースで望ましい状態を達成することは困難です。

例えば:

UserService には、新しいユーザーの登録と電子メールの送信という複数の責任があります。 ユーザー登録のテスト中に、メール サービスを処理する必要があり、その逆も同様です。

2 つ以上の無関係な依存関係を持つ UserService を想像してみてください。 これらの依存関係には独自の依存関係があります。 判読不能で、無関係な情報で肥大化し、非常に理解しにくいテストになってしまいます。 したがって、すべてのクラスには、変更する責任と理由が 1 つだけある必要があります。 変更する理由が 1 つしかないクラスは、単一責任の原則と呼ばれる 5 つのソフトウェア設計原則の 1 つです。

SOLID の原則について詳しくは、こちらをご覧ください。

結論

ソフトウェア設計のベスト プラクティスに従うコードベースにより、単体テストの記述がはるかに管理しやすくなります。

一方で、前述のアンチパターンを使用してコードベースの作業単体テストを作成することは、非常に困難であり、場合によっては不可能ですらあります。 優れたテスト可能なコードを作成するには、多くの練習、規律、および余分な努力が必要です。 テスト可能なコードの最も重要な利点は、テストのしやすさと、そのコードを理解し、維持し、拡張できることです。

このブログが、テスト可能なコードを作成するのに役立つことを願っています。