web-dev-qa-db-ja.com

単体テストのためだけに、クラスに個別のコンストラクターが必要ですか?

私は2つのコンストラクターを持つクラスを作成するのが好きです。本番コードで使用されるプライマリコンストラクターと、単体テスト用のデフォルトコンストラクターです。これを行うのは、プライマリコンストラクターが外部リソースを消費する他の具体的な依存関係を作成するためです。単体テストではすべてを行うことはできません。

だから私のクラスは次のようになります:

public class DoesSomething : BaseClass
{
    private Foo _thingINeed;
    private Bar _otherThingINeed;
    private ExternalResource _another;

    public DoesSomething()
    {
        // Empty constructor for unit tests
    }

    public DoesSomething(string someUrl, string someThingElse, string blarg)
    {
         _thingINeed = new Foo(someUrl);
         _otherThingINeed = Foo.CreateBar(blarg);
         _another = BlargFactory.MakeBlarg(_thingINeed, _otherThingINeed.GetConfigurationValue("important");
    }
}

これは、クラスの単体テストを記述できるように、多くのクラスで従うパターンです。私は常にデフォルトのコンストラクターにコメントを含めて、他の人がそれが何のためにあるかがわかるようにします。これはテスト可能なクラスを書くのに良い方法ですか、それとももっと良いアプローチがありますか?


これは、「依存関係が挿入されたコンストラクターと、インスタンス化する別のコンストラクターが必要ですか」の複製である可能性があることが示唆されました。そうではありません。その質問は、依存関係を作成する別の方法についてです。デフォルトの単体テストコンストラクターを使用すると、依存関係がまったく作成されません。次に、ほとんどのメソッドを公開し、依存関係を使用しません。それらは私がテストするものです。

6
Scott Hannen

ユニットテスト用に個別のコードパスを作成しないでください。ユニットテストで1つのコードパスをテストし、本番環境で別のコードパスを実行するため、ユニットテストの目的が損なわれます。

なんらかの理由で、私はあなたの質問の一部を繰り返し繰り返しているだけなので、不十分だと思われます。単体テストで別のコードを実行していることはすでにわかっています。あなたのコンストラクタに入れたコメントはそれを言っています。他人の携帯電話を携帯するのが適切かどうかを尋ねるようなものです。携帯電話はあなたのものではなく、自分のものであり、あなたはそれを手に入れたいからです。

コンセンサスからの議論は役に立ちますか?誰もそれをしないのでそれをしないでください。もっとよく知っている人はショックを受けるでしょう。さらに悪いことに、よく知らない人はそれから学ぶかもしれません、それは悪いことです...私はまだサークルに行っているような気がします。しかし、うまくいけば、他の人があなたがそれをしてはいけないと言うのに、いくらか重みがあります。

多分私はあなたにもっと良い方法があることをあなたに示すことができました:依存関係の逆転と依存関係の注入。依存関係の逆転とは、具体的な実装ではなく、抽象化(インターフェースなど)に依存することを意味します。クラスが独自の依存関係を作成している場合、これは不可能です。ここで、依存関係の注入が行われます。

依存関係のインスタンスを作成する代わりに、クラスがそれらを使用する方法を記述する抽象化を定義します。たとえば、ExternalResourceがファイルのダウンロード元である場合、次のようなインターフェースを作成できます。

public interface IFileStorage
{
    byte[] GetFile(string filePath);
}

次に、次のようにクラスを作成します。

public class DoesSomething : BaseClass
{
    private readonly IFileStorage _fileStorage;

    public DoesSomething(IFileStorage fileStorage)
    {
        _fileStorage = fileStorage;
    }
}

これで、別個のコンストラクターは必要ありません。単体テストではrealコンストラクターを呼び出し、モックまたはテストdoubleを渡すことができます。つまり、IFileStorageの実装は、希望どおりの結果を返します。このようにして、実際のIFileStorageの実装を使用せずに、ファイルにこれまたはそれが含まれるか、まったく含まれない場合のクラスの動作のテストを記述できます。

たとえば、IFileStorageが空のバイト配列を返した場合にクラスがどのように動作するかを確認するにはどうすればよいでしょうか。 Moqのようなフレームワークを使用するか、次のようにします。

public class FileStorageThatReturnsEmptyArray : IFileStorage
{
    public byte[] GetFile(string filePath)
    {
        return new byte[] {};
    }
}

次に、被験者は次のようになります。

var subject = new DoesSomething(new FileStorageThatReturnsEmptyArray());

それは良くないですか?ユニットテストはクラスをエンドツーエンドでテストし、本番環境での動作とまったく同じように動作できるため、価値があります。

確かに、本番環境では、テストダブルの代わりにIFileStorageの具体的な実装があります。しかし重要なのは、DoesSomethingにとってはすべて同じことです。 1つのIFileStorageは別のものと同じです。

「本当の」コードをテストするべきだと私が言ったので、それは矛盾ですが、今私はモックの使用を勧めていますか?いいえ。問題は単体テストに関するものでした。つまり、DoesSomethingをテストしたいので、モックを使用してそのクラスをテストします。他のクラスの追加の単体テストまたは統合テストを作成して、それらを一緒にテストできます。この回答の範囲では、DoesSomethingをテストする場合、そのクラスの非プロダクションメソッドをテストすることで自分をだましたくないので、以下と同じだと考えます。生産方法のテスト。 (「プロダクション」と「非プロダクション」の方法は、実際のものではありません。)

他の開発者がこの問題をどのように解決したかを示すことが役立つことを願っています。

17
Scott Hannen

この回答がスコットハンネンの回答と異なるかどうかは不明ですが、用語と意図には違いがあります。


モックとスタブの違いは何ですか? からインスピレーションを得て(コピーして貼り付けて)、コンストラクターに渡された依存関係を使用しないコードをテストしています。あなたが探しているのはダミーオブジェクトです。

From Mocks Are n't Stubs by Martin Fowler(強調、私のもの):

  • ダミーオブジェクトは渡されますが、実際には使用されません。通常、これらはパラメーターリストを埋めるためにのみ使用されます。
  • 偽のオブジェクトには実際に機能する実装がありますが、通常はいくつかのショートカットを使用するため、本番環境には適していません(メモリ内データベースが良い例です)。
  • スタブは、テスト中に行われた呼び出しに対する定型の回答を提供します。通常、テスト用にプログラムされたもの以外ではまったく応答しません。
  • スパイは、それらがどのように呼び出されたかに基づいていくつかの情報も記録するスタブです。この1つの形式は、送信されたメッセージの数を記録する電子メールサービスです。
  • ここでモックとは、モックと呼ばれるものです。受信が予想される呼び出しの仕様を形成する、事前にプログラムされたオブジェクトです。

コンストラクタがあります。オブジェクトを渡す必要があります。テストはそのオブジェクトを使用していません。実際の依存関係と同じパブリックインターフェイスを含むDummyオブジェクトが必要ですが、実際にはanythingを実行しません—実際には、メソッドが呼び出されるたびに例外をスローできます。

ここから先の方法を示すために、コード例を使用します。

まず、実際に必要以上のことを行うコンストラクターがあります。これをリファクタリングして、実際のオブジェクトを渡すことができる新しいコンストラクタを作成します。

public class DoesSomething : BaseClass
{
    private Foo _thingINeed;
    private Bar _otherThingINeed;
    private ExternalResource _another;

    public DoesSomething(string someUrl, string someThingElse, string blarg)
        : this(new Foo(someUrl), Foo.CreateBar(blarg), BlargFactory.MakeBlarg(_thingINeed, _otherThingINeed.GetConfigurationValue("important"))
    {
    }

    public DoesSomething(Foo thing1, Bar thing2, ExternalResource thing3)
    {
         _thingINeed = thing1;
         _otherThingINeed = thing2;
         _another = thing3;
    }
}

違いに注意してください:

  1. 空のコンストラクタはありません
  2. 既存のコンストラクタのシグネチャはまだ存在しており、既存のコードとの下位互換性をサポートしています
  3. FooBarおよびExternalResourceを取り込む新しいコンストラクターが追加されました
  4. 古いコンストラクターが新しいコンストラクターを呼び出し、初期化ロジックが重複していないことを確認します

ExternalResourceの作成は少し複雑だと思います。この依存関係のインターフェースが必要です。これは別の変更につながります。

public class DoesSomething : BaseClass
{
    // ...

    public DoesSomething(Foo thing1, Bar thing2, IExternalResource thing3)
    {
        // ...
    }
}

public interface IExternalResource
{
    // ...
}

public class ExternalResource : IExternalResource
{
    // ...
}

次に、FooBarがコンストラクターで実際の作業を行わないようにする必要があります。さらに、FooBarがこのクラスのインスタンスメソッドで使用されていない場合は、フィールドとして保持しないでください。これにより、このクラスの依存関係がさらに簡素化され、単体テストのモックがさらに容易になります。

つまり、空のダムオブジェクトを作成するだけです。

internal class DummyExternalResource : IExternalResource
{
    public void Something()
    {
        throw new NotImplementedException();
    }
}

例外をスローするだけのIExternalResourceの必要なすべてのメソッドでダミーオブジェクトを作成するだけです。次に、テストで:

// Arrange
var foo = new Foo("fakeFile.text");
var bar = new Bar("fake blarg");
var externalResource = new DummyExternalResource();
var something = new DoesSomething(foo, bar, externalResource);

// Act
something.Bazz();

// Assert
Assert.IsTrue(something.FooBar);

将来、コードの変更でIExternalDependencyのメソッドを呼び出す必要がある場合は、テスト対象のメソッドの新しい依存関係が導入されているため、失敗したテストが表示されます。これにより、開発者はこのメソッドのテストカバレッジを再評価する必要があります。

2
Greg Burghardt