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


54

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

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

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

テスト駆動開発と呼ばれるものがあることを認識しています。ただし、テストを通じて設計を推進するよりも、設計段階でテストを念頭に置いてコードを設計することにもっと興味があります。これが理にかなっていることを願っています。

このトピックに関連するもう1つの質問は、既存の製品/プロジェクトをリファクタリングし、各モジュールのユニットテストケースを作成できるようにするためにコードとデザインを変更してもよいかどうかです。


3
これを見てください:googletesting.blogspot.in/2008/08/…–
VS1

ありがとうございました。私は記事を読み始めたばかりで、それはすでに理にかなっています。

1
これは私のインタビューの質問の1つです(「簡単に単体テストを行うためにコードをどのように設計しますか?」)。単体テスト、モック/スタブ、OOD、および潜在的にTDDを理解しているかどうかを単独で示してくれます。悲しいことに、答えは通常「テストデータベースを作成する」ようなものです。
クリスピットマン

回答:


56

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

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

この原則に従うことにより、異なるオブジェクトで同じ機能をテストするために作成する必要があるテストの数を減らすことで、デザインのテスト容易性が向上します。また、通常、単独でテストするのが簡単な機能の小さな部分になります。

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

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

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

これは、テスト可能なコードを記述するのに非常に大きなことです。LSPに準拠する設計では、予測される動作を変更せずに、実物の代わりに「モック」オブジェクトを使用できるため、小さなコードを自信を持って単独でテストできます。システムはプラグインされた実際のオブジェクトで動作します。

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

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

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

繰り返しになりますが、これはテスト対象のオブジェクトに「本番」実装の代わりに依存関係のモック実装を挿入することができるため、テスト可能性にとって非常に大きなものです。生産中。これは、「単独で」単体テストを行うための鍵です。


16

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

正しく適用された場合、はい。ありますジェフのブログ記事ではSOLID原則を説明本当に短い方法は(言及したポッドキャストがあまりにも聴く価値がある)、私は長い説明はあなたを投げている場合はそちらを与えることをお勧めします。

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

  • インターフェイス分離の原則 -少数の汎用インターフェイスではなく、多くのクライアント固有のインターフェイスを優先する必要があります。これは単一責任原則と組み合わせて機能/タスク指向のクラスを設計するのに役立ちます。その結果、テストがはるかに簡単になります(より一般的なクラス、または頻繁に悪用される"マネージャー"および"コンテキスト"に比べて) 、複雑さの軽減、よりきめの細かい明白なテスト。つまり、小さなコンポーネントは簡単なテストにつながります。
  • 依存関係反転の原則 -実装ではなく、契約による設計。これは、複雑なオブジェクトをテストセットアップするために依存関係のグラフ全体を必要としないことを理解するときに最も役立ちますが、インターフェイスを単純にモックして完了できます。

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

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

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

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

とにかく単体テストは、テストが難しいコードに対しても書く価値があります。コードが機能しているが、単体テストされていない場合、適切な解決策は、コードのテストを作成してから変更導入することです。ただし、より簡単にテストできるようにテスト済みのコードを変更することは、経営陣がお金をかけたくない場合があることに注意してください(おそらく、ビジネス価値をほとんどもたらさないと聞きます)。


iaw高い凝集力と低い結合
jk。

8

あなたの最初の質問:

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

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

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

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


4

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

かつては論理が複雑で、多くの条件があり、フィールドの役割を理解することが困難だったクラスを単体テストする必要がありました。

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

状態が明示的であるという事実により、コードの可能なすべてのパス(状態遷移)を列挙しやすくなり、各ユニットの単体テストを記述しやすくなりました。

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


3

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

  • 単一責任の原則 -各クラスは1つのことと1つのことだけを行います。値の計算、ファイルのオープン、文字列の解析など。したがって、入力と出力の量、および決定点は非常に最小限に抑える必要があります。これにより、テストを簡単に作成できます。
  • リスコフ置換の原則 -コードの望ましいプロパティ(期待される結果)を変更せずに、スタブとモックで置換できる必要があります。
  • インターフェース分離の原則 -インターフェースによって接点を分離することにより、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またはその他のモックフレームワークを使用して、その仮想メソッドをオーバーライドし、デザインのインターフェイスを返すことができます。しかし、実装コードに関する限り、ファクトリは変更されていません。この方法で実装の詳細の多くを隠すこともできます。実装コードはインターフェースの構築方法を気にせず、インターフェースを取り戻すことだけが重要です。

これを少し拡張したい場合は、The Art of Unit Testingを読むことをお勧めします。この原則の使用方法に関する優れた例を示しており、非常に簡単に読むことができます。


1
これは、依存性の「反転」原理と呼ばれ、「注入」原理ではありません。
マティアスリュックガードローレンツェン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.