web-dev-qa-db-ja.com

テスト可能なコードを促進する設計原則は何ですか? (テスト可能なコードの設計とテストによる設計の推進)

私が取り組んでいるプロジェクトのほとんどは、開発と単体テストを分離して検討しているため、後で単体テストを書くことは悪夢になります。私の目的は、高レベルおよび低レベルの設計フェーズ自体の間、テストを念頭に置くことです。

テスト可能なコードを促進する明確に定義された設計原則があるかどうか知りたい。私が最近理解するようになったそのような原則の1つは、依存性注入と制御の反転による依存性反転です。

SOLIDと呼ばれるものがあることを読みました。 SOLID原則に従うと間接的に簡単にテスト可能なコードが生成されるかどうかを理解したいですか?そうでない場合、テスト可能なコードを促進する明確に定義された設計原則はありますか?

テスト駆動開発と呼ばれるものがあることを知っています。ただし、テストを通じて設計を進めるよりも、設計フェーズ自体の間にテストを念頭に置いてコードを設計することに関心があります。これが理にかなっているといいのですが。

このトピックに関連するもう1つの質問は、各モジュールの単体テストケースを記述できるようにするために、既存の製品/プロジェクトをリファクタリングし、コードと設計に変更を加えてもよいかどうかです。

55
CKing

はい、SOLIDは簡単にテストできるコードを設計するための非常に良い方法です。短い入門書として:

S-単一責任の原則:オブジェクトは正確に1つのことを行う必要があり、その1つのことを行うコードベース内の唯一のオブジェクトである必要がありますたとえば、ドメインクラスを取り、請求書を言います。 Invoiceクラスは、システムで使用される請求書のデータ構造とビジネスルールを表す必要があります。コードベースで請求書を表す唯一のクラスである必要があります。これをさらに細かく分解すると、メソッドには1つの目的があり、コードベースでこのニーズを満たす唯一のメソッドである必要があると言えます。

この原則に従うことで、異なるオブジェクトで同じ機能をテストするために作成する必要があるテストの数を減らすことで、設計のテスト容易性を高め、通常は、個別にテストするのが簡単な機能の小さな断片になってしまいます。

O-オープン/クローズの原則:クラスは拡張に対してオープンであるが、変更のためにクローズされるべきである。オブジェクトが存在し、正しく機能したら、理想的にはそのオブジェクトに戻って新しい機能を追加する変更を加える必要はありません。代わりに、オブジェクトを派生させるか、新しいまたは異なる依存関係の実装をプラグインすることにより、オブジェクトを拡張して、新しい機能を提供する必要があります。これにより、回帰が回避されます。オブジェクトが他の場所ですでに使用されているので、オブジェクトの動作を変更せずに、必要なときに必要な場所に新機能を導入できます。

この原則を順守することで、一般に「モック」を許容するコードの機能が向上し、新しい動作を予測するためにテストを書き直す必要もなくなります。オブジェクトの既存のテストはすべて、拡張されていない実装でも機能するはずですが、拡張された実装を使用した新しい機能の新しいテストも機能するはずです。

L-リスコフ置換の原則:クラスBに依存するクラスAは、違いを知らなくてもX:Bを使用できるはずですこれは基本的に、依存関係として使用するものはすべて、依存クラスで見られるのと同様の動作をする必要があることを意味します。短い例として、ConsoleWriterによって実装されるWrite(string)を公開するIWriterインターフェイスがあるとします。次に、代わりにファイルに書き込む必要があるため、FileWriterを作成します。その際、FileWriterをConsoleWriterと同じ方法で使用できることを確認する必要があります(つまり、依存関係がそれとやり取りできる唯一の方法はWrite(string)を呼び出すことです)。これにより、FileWriterがそれを行うために必要な追加情報ジョブ(書き込むパスやファイルなど)は、依存ファイル以外の場所から提供する必要があります。

LSPに準拠する設計では、期待される動作を変更せずに、いつでも実際のオブジェクトを「モック」オブジェクトに置き換えることができるため、これはテスト可能なコードを書くために非常に大きなものです。システムはプラグインされた実際のオブジェクトで動作します。

I-インターフェース分離の原則:インターフェースは、インターフェースによって定義された役割の機能を提供するために実行可能な限り少ないメソッドを持つ必要があります。簡単に言えば、より小さなインターフェイスの方が、より大きなインターフェイスの数よりも優れています。これは、インターフェースが大きいほど変更する理由が多くなり、コードベースの他の場所で必要のない変更が多く発生するためです。

ISPへの準拠により、テスト対象のシステムの複雑さおよびそれらのSUTの依存関係の複雑さが軽減され、テスト容易性が向上します。テストするオブジェクトがDoOne()、DoTwo()、およびDoThree()を公開するインターフェイスIDoThreeThingsに依存している場合、オブジェクトがDoTwoメソッドのみを使用している場合でも、3つのメソッドすべてを実装するオブジェクトをモックする必要があります。ただし、オブジェクトがIDoTwo(DoTwoのみを公開する)にのみ依存している場合は、その1つのメソッドを持つオブジェクトをより簡単にモックできます。

D-依存関係の逆転の原則:具体化と抽象化は他の具体化に依存するべきではなく、抽象化に依存するべきです。この原則は、疎結合の原則を直接適用します。オブジェクトは、オブジェクトが何であるかを知る必要はありません。代わりに、オブジェクトが何をするかを気にする必要があります。したがって、オブジェクトやメソッドのプロパティやパラメータを定義するときは、具体的な実装を使用するよりも、インターフェイスや抽象基本クラスを使用する方が常に推奨されます。これにより、使用方法を変更することなく、1つの実装を別の実装に交換できます(LSPにも従う場合は、DIPと連動します)。

この場合も、テスト対象のオブジェクトに「本番」実装ではなく、依存関係のモック実装を注入しながら、オブジェクトの正確な形式でオブジェクトをテストできるため、これはテスト容易性にとって非常に大きなものです。生産中。これは、「単独で」単体テストを行うための鍵です。

58
KeithS

SOLIDと呼ばれるものがあることを読みました。 SOLIDの原則に従うと間接的に簡単にテストできるコードが得られるかどうかを理解したいですか?

正しく適用されれば、はい。 Jeffによるブログ投稿 説明SOLID原則をreallyで簡単に説明します(言及されているポッドキャストも聞く価値があります)。長い説明があなたをスローさせているならそこに。

私の経験から、テスト可能なコードの設計においてSOLIDの2つの原則が大きな役割を果たします。

  • インターフェース分離の原則-少数の汎用インターフェースではなく、多くのクライアント固有インターフェースを優先する必要があります。これは単一責任の原則と組み合わせて使用​​することで、機能/タスク指向のクラスを設計するのに役立ちます。より一般的なもの、または頻繁に乱用される"managers"および"contexts")-依存関係が少なく、複雑さが少なく、きめ細かい、明白なテスト。つまり、小さなコンポーネントは単純なテストにつながります。
  • 依存関係の逆転の原則-実装ではなく、契約による設計。これは、複雑なオブジェクトをテストし、依存関係のグラフ全体が不要であることに気づくときに最も役立ちます設定するだけですが、インターフェイスをモックして、それで終了できます。

これら2つは、テスト容易性を設計するときに最も役立つと思います。残りのものにも影響がありますが、それほど大きくはないと思います。

(...)各モジュールの単体テストケースを作成できるようにするために、既存の製品/プロジェクトをリファクタリングし、コードと設計に変更を加えても大丈夫ですか?

既存の単体テストがなければ、それは単に置かれます-トラブルを求めます。単体テストは、コードworksの保証です。適切なテストカバレッジがある場合、重大な変更の導入がすぐにわかります。

既存のコードを変更するユニットテストを追加するにする場合、これにより、テストがないギャップが生じます。まだですが、コードはすでに変更されています。当然のことながら、変更が壊れたことの手がかりはないかもしれません。これは避けたい状況です。

ユニットテストは、テストが難しいコードに対しても、とにかく書く価値があります。コードが動作しているでも単体テストされていない場合は、適切な解決策としてコードのテストを記述し、thenで変更を導入します。ただし、テストを容易にするためにテスト済みのコードを変更することは、経営陣が費用をかけたくない場合があることに注意してください(おそらく、ビジネス上の価値がほとんどないことがわかります)。

16
k.m

最初の質問:

確かにSOLIDは進むべき道です。テスト可能性に関して、SOLID頭字語の2つの最も重要な側面は、S(単一の責任)とD(依存性注入)です。

単一の責任クラスは実際には1つのことだけを行い、1つのことだけを行う必要があります。ファイルを作成し、入力を解析して、ファイルに書き込むクラスは、すでに3つのことを行っています。クラスが1つのことだけを実行する場合は、それが何を期待するかを正確に理解しており、そのためのテストケースの設計はかなり簡単です。

Dependency Injection(DI):これにより、テスト環境を制御できます。コード内に外国のオブジェクトを作成する代わりに、クラスコンストラクターまたはメソッド呼び出しを介してそれを注入します。ユニットテストを行うときは、実際のクラスをスタブまたはモックに置き換えるだけで、完全に制御できます。

2番目の質問:理想的には、コードをリファクタリングする前に、コードの機能を文書化するテストを記述します。このようにして、リファクタリングが元のコードと同じ結果を再現することを文書化できます。ただし、問題は機能するコードのテストが難しいことです。これは古典的な状況です!私のアドバイスは、ユニットテストの前にリファクタリングについて慎重に検討してください。できれば;動作するコードのテストを記述してから、コードをリファクタリングし、テストをリファクタリングします。私はそれが数時間かかることを知っていますが、あなたは、リファクタリングされたコードが古いものと同じことをすることはより確かです。そうは言っても、私は何度も諦めてきました。クラスは非常に醜く乱雑で、テストをテスト可能にする唯一の方法は書き換えです。

8
Morten

疎結合を実現することに重点を置いた他の回答に加えて、複雑なロジックのテストについて一言述べたいと思います。

かつては、ロジックが複雑で、多くの条件があり、フィールドの役割を理解するのが困難なクラスをユニットテストする必要がありました。

このコードをstate machineを表す多くの小さなクラスに置き換えました。前のクラスのさまざまな状態が明示的になったため、ロジックの追跡がはるかに簡単になりました。各状態クラスは他のクラスから独立しているため、簡単にテストできました。

状態が明示的であるという事実は、コードのすべての可能なパス(状態遷移)を列挙することを容易にし、したがって、それぞれの単体テストを書くことが容易になりました。

もちろん、すべての複雑なロジックをステートマシンとしてモデル化できるわけではありません。

4
barjak

SOLIDは素晴らしいスタートです。私の経験では、SOLIDの4つの側面がユニットテストで本当にうまく機能します。

  • 単一の責任の原則-各クラスは1つの処理と1つの処理のみを実行します。値の計算、ファイルのオープン、文字列の解析など。したがって、入力と出力の量、および決定ポイントは非常に最小限に抑える必要があります。これにより、テストを簡単に作成できます。
  • Liskov置換の原則-コードの望ましいプロパティ(期待される結果)を変更せずに、スタブとモックで置換できるはずです。
  • インターフェース分離の原則-インターフェースによって接点を分離することで、Moqなどのモックフレームワークを使用してスタブやモックを非常に簡単に作成できます。具象クラスに依存する代わりに、インターフェースを実装する何かに単に依存しています。
  • 依存性注入の原則-これにより、コンストラクター、プロパティ、またはメソッドのパラメーターを介して、これらのスタブとモックをコードに注入できますテストしたい。

また、さまざまなパターン、特にファクトリーパターンも調べます。インターフェースを実装する具象クラスがあるとします。具象クラスをインスタンス化するファクトリを作成しますが、代わりにインターフェースを返します。

public interface ISomeInterface
{
    int GetValue();
}  

public class SomeClass : ISomeInterface
{
    public int GetValue()
    {
         return 1;
    }
}

public interface ISomeOtherInterface
{
    bool IsSuccess();
}

public class SomeOtherClass : ISomeOtherInterface
{
     private ISomeInterface m_SomeInterface;

     public SomeOtherClass(ISomeInterface someInterface)
     {
          m_SomeInterface = someInterface;
     }

     public bool IsSuccess()
     {
          return m_SomeInterface.GetValue() == 1;
     }
}

public class SomeFactory
{
     public virtual ISomeInterface GetSomeInterface()
     {
          return new SomeClass();
     }

     public virtual ISomeOtherInterface GetSomeOtherInterface()
     {
          ISomeInterface someInterface = GetSomeInterface();

          return new SomeOtherClass(someInterface);
     }
}

テストでは、Moqまたは他のモックフレームワークを使用して、その仮想メソッドをオーバーライドし、設計のインターフェイスを返すことができます。しかし、実装コードに関する限り、ファクトリは変更されていません。このようにして、実装の詳細の多くを非表示にすることもできます。実装するコードは、インターフェースの構築方法を気にしません。重要なのは、インターフェースを取り戻すことだけです。

これについて少し詳しく知りたい場合は、 ユニットテストの芸術 をお勧めします。これは、この原則の使用方法に関するいくつかの優れた例を示しています。

3
bwalk2895