ドメインオブジェクトの作成をテストする単体テスト


11

次のような単体テストがあります。

[Test]
public void Should_create_person()
{
     Assert.DoesNotThrow(() => new Person(Guid.NewGuid(), new DateTime(1972, 01, 01));
}

ここでPersonオブジェクトが作成された、つまり検証が失敗しないと断言しています。たとえば、Guidがnullの場合、または生年月日が1900年1月1日より前の場合、検証は失敗し、例外がスローされます(テストが失敗したことを意味します)。

コンストラクターは次のようになります。

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

これはテストのための良いアイデアですか?

:ドメインモデルの単体テストに古典的なアプローチを採用している場合は、それが何らかの意味を持っています。


コンストラクターには、初期化中にアサートする価値のあるロジックがありますか?
ライヴ

2
コンストラクタをテストする必要はありません!!! 建設はまっすぐ進むべきです。Guid.NewGuid()またはDateTimeのコンストラクターで失敗することを期待していますか?
ivenxu

@Laiv、質問の更新をご覧ください。
w0051977

1
共有したものとしてテストを実装することは何の価値もありません。ただし、逆もテストします。birthDateがエラーを引き起こすケースをテストします。これは、制御およびテストするクラスの不変式です。
ライヴ

3
テストは問題ないように見えますが、1つだけ名前を残しています。Should_create_person?人を作成するのは何ですか?のような意味のある名前を付けCreating_person_with_valid_data_succeedsます。
デビッドアルノ

回答:


18

これは有効なテストです(かなり熱心ですが)。私は時々コンストラクターロジックをテストするためにそれを行いますが、Laivがコメントで述べたように、理由を自問する必要があります。

コンストラクタが次のようになっている場合:

public Person(Guid guid, DateTime dob)
{
  this.Guid = guid;
  this.Dob = dob;
}

それがスローされるかどうかをテストすることには多くのポイントがありますか?パラメータが正しく割り当てられているかどうかは理解できますが、テストはかなりやり過ぎです。

ただし、テストで次のようなことが行われる場合:

public Person(Guid guid, DateTime dob)
{
  if(guid == default(Guid)) throw new ArgumentException("Guid is invalid");
  if(dob == default(DateTime)) throw new ArgumentException("Dob is invalid");

  this.Guid = guid;
  this.Dob = dob;
}

その後、テストの関連性が高まります(実際にコードのどこかに例外をスローしているため)。

一般的に言って、コンストラクタに多くのロジックを含めることは悪い習慣です。基本的な検証(上記のnull / defaultチェックなど)は大丈夫です。しかし、データベースに接続して誰かのデータをロードしている場合、そこからコードの臭いがし始めます...

このため、コンストラクターをテストする価値がある場合(多くのロジックがあるため)、他の何かが間違っている可能性があります。

ビジネスロジックレイヤーでこのクラスをカバーする他のテストがほぼ確実に行われます。コンストラクターと変数の割り当ては、ほぼ確実にこれらのテストから完全にカバーされます。したがって、コンストラクタ専用の特定のテストを追加することは無意味かもしれません。しかし、何も白黒ではなく、コードレビューを行っていればこれらのテストに対して何もしませんでしたが、ソリューションの他の場所のテスト以上に価値を追加するかどうかは疑問です。

あなたの例では:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

検証を行うだけでなく、ベースコンストラクターも呼び出します。コンストラクター/検証ロジックが2つのクラスに分割され、可視性が低下し、予期しない変更のリスクが高まるため、これらのテストを行うより多くの理由があります。

TLDR

これらのテストにはいくつかの価値がありますが、検証/割り当てロジックはソリューションの他のテストでカバーされる可能性があります。重要なテストを必要とするこれらのコンストラクターに多くのロジックがある場合、そこに潜んでいる厄介なコードの匂いがあることを示唆しています。


@レイス、私の質問の更新を参照してください
w0051977

この例では、ベースコンストラクターを呼び出していることに気付きました。これにより、テストの価値が高まります。コンストラクターロジックは2つのクラスに分割されるため、変更のリスクがわずかに高くなるため、テストする理由が増えます。
リース

「ただし、テストで次のようなことが行われる場合:」 < コンストラクターが次のようなことを行う場合」という意味ではありませんか?
Kodosジョンソン

「これらのテストには何らかの価値があります」-とにかく興味深いことに、この値はPersonBirthdate、生年月日検証を実行する人のdob(例えば)を表す新しいクラスを使用することで、このテストを冗長にできることを示しています。同様に、GuidチェックをIdクラスに実装できます。これはPersonnullref を除き、無効なデータを使用して構築することはできないため、コンストラクターにその検証ロジックを実際に持つ必要がないことを意味します。もちろん、他の2つのクラスのテストを作成する必要があります:)
スティーブンバーン

12

ここではすでに良い答えですが、もう1つ言及する価値があると思います。

「本で」TDDを行う場合、コンストラクターが実装される前であっても、コンストラクターを呼び出すテストを最初に記述する必要があります。そのテストは、コンストラクターの実装内に検証ロジックがない場合でも、実際に提示したテストのように見える可能性があります。

また、TDDの場合、次のように最初に別のテストを記述する必要があります。

  Assert.Throws<ArgumentException>(() => new Person(Guid.NewGuid(), 
        new DateTime(1572, 01, 01));

DateTime(1900,01,01)コンストラクタにチェックを追加する前に

TDDのコンテキストでは、示されているテストは完全に理にかなっています。


考慮されなかった素敵な角度!
-Liath

1
これは、TDDのそのような厳格な形式が時間の無駄である理由を私に示しています:コードが記述された、テストに価値があるはずです、または、コードのすべての行を2回、1回はアサーションとして、1回はコードとして書くだけです コンストラクター自体は、テストが必要なロジックの一部ではないと主張します。「1900年以前に生まれた人は表現できてはならない」というビジネスルールはテスト可能であり、コンストラクターはそのルールが実装される場所ですが、空のコンストラクターのテストはいつプロジェクトに値を追加しますか?
IMSoP

それは本で本当にtddですか?インスタンスを作成し、そのメソッドをコードですぐに呼び出します。次に、そのメソッドのテストを作成し、それを行うことにより、そのメソッドのインスタンスも作成する必要があるため、コンストラクターとメソッドの両方がそのテストでカバーされます。コンストラクターに何らかのロジックがない限り、その部分はLiathによってカバーされます。
ラファウŁużyński18年

@RafałŁużyński:TDD "by the book"は最初にテストを書くことです。実際には、常に失敗したテストを最初に書くことを意味します(失敗としてもカウントをコンパイルしません)。そのため、最初にコンストラクタがない場合でも、コンストラクタを呼び出すテストを記述します。次に、コンパイルを試みます(失敗します)。次に、空のコンストラクターを実装し、コンパイルし、テストを実行します(result = green)。次に、最初の失敗したテストを作成して実行します-result = red、次にテストを再び「グリーン」にする機能を追加します。
Doc Brown

もちろん。最初に実装を作成してからテストするという意味ではありませんでした。上のレベルでそのコードの「使用法」を書き、そのコードをテストしてから、それを実装します。私は通常「Outside TDD」を行っています。
ラファウŁużyński18年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.