「セッターなし」の世界での単体テスト


23

私は自分自身をDDDの専門家とは考えていませんが、ソリューションアーキテクトとして、可能な限りベストプラクティスを適用しようとしています。私は、DDDのノー(パブリック)セッター「スタイル」の賛否両論について多くの議論があることを知っており、議論の両側を見ることができます。私の問題は、スキル、知識、経験が非常に多様なチームで働いているということです。つまり、すべての開発者が「正しい」方法で作業することを信頼することはできません。たとえば、オブジェクトの内部状態の変更がメソッドによって実行されるがパブリックプロパティセッターを提供するようにドメインオブジェクトが設計されている場合、誰かがメソッドを呼び出す代わりにプロパティを設定することは避けられません。この例を使用します。

public class MyClass
{
    public Boolean IsPublished
    {
        get { return PublishDate != null; }
    }

    public DateTime? PublishDate { get; set; }

    public void Publish()
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        PublishDate = DateTime.Today;

        Raise(new PublishedEvent());
    }
}

私の解決策は、プロパティセッターをプライベートにすることです。これは、オブジェクトをハイドレートするために使用しているORMがリフレクションを使用してプライベートセッターにアクセスできるためです。ただし、単体テストを作成しようとすると問題が発生します。たとえば、再発行できないという要件を検証する単体テストを作成する場合、オブジェクトが既に発行されていることを示す必要があります。確かに2回Publishを呼び出すことでこれを行うことができますが、私のテストでは、最初の呼び出しでPublishが正しく実装されていると想定しています。それは少し臭いようです。

次のコードを使用して、シナリオをもう少し現実的にしましょう。

public class Document
{
    public Document(String title)
    {
        if (String.IsNullOrWhiteSpace(title))
            throw new ArgumentException("title");

        Title = title;
    }

    public String ApprovedBy { get; private set; }
    public DateTime? ApprovedOn { get; private set; }
    public Boolean IsApproved { get; private set; }
    public Boolean IsPublished { get; private set; }
    public String PublishedBy { get; private set; }
    public DateTime? PublishedOn { get; private set; }
    public String Title { get; private set; }

    public void Approve(String by)
    {
        if (IsApproved)
            throw new InvalidOperationException("Already approved.");

        ApprovedBy = by;
        ApprovedOn = DateTime.Today;
        IsApproved = true;

        Raise(new ApprovedEvent(Title));
    }

    public void Publish(String by)
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        if (!IsApproved)
            throw new InvalidOperationException("Cannot publish until approved.");

        PublishedBy = by;
        PublishedOn = DateTime.Today;
        IsPublished = true;

        Raise(new PublishedEvent(Title));
    }
}

以下を検証する単体テストを書きたい:

  • ドキュメントが承認されない限り公開できません
  • ドキュメントを再発行できません
  • 公開されると、PublishedBy値とPublishedOn値が適切に設定されます
  • 公開されると、PublishedEventが発生します

セッターへのアクセスなしでは、オブジェクトをテストの実行に必要な状態にすることはできません。セッターへのアクセスを開くと、アクセスを防止する目的が無効になります。

この問題をどのように解決しますか?


私がこれについて考えるほど、あなたの問題全体が副作用のある方法を持っていると思う。むしろ、可変不変オブジェクト。DDDの世界では、このオブジェクトの内部状態を更新するのではなく、承認と発行の両方から新しいDocumentオブジェクトを返すべきではありませんか?
pdr

1
簡単な質問、使用しているO / RM。私はEFの大ファンですが、保護者としてセッターを宣言すると、少し間違った方法でこすられます。
マイケルブラウン

私は、私が苦労して担当したフリーレンジ開発のため、現在ミックスを持っています。いくつかのADO.NETは、AutoMapperを使用して、いくつかのLinq-to-SQLモデルであるDataReaderからハイドレートします。 )およびいくつかの新しいEFモデル。
-SonOfPirate

Publishを2回呼び出すことはまったく臭いがなく、その方法です。
ピョートルペラ

回答:


27

オブジェクトをテストの実行に必要な状態にすることはできません。

オブジェクトをテストの実行に必要な状態にできない場合、オブジェクトを製品コードの状態にできないため、その状態をテストする必要はありません。明らかに、これはあなたの場合には当てはまりません。オブジェクトを必要な状態にすることができ、承認を呼び出すだけです。

  • ドキュメントが承認されない限り公開できません。承認を呼び出す前に発行を呼び出すと、オブジェクトの状態を変更せずに正しいエラーが発生するというテストを記述します。

    void testPublishBeforeApprove() {
        doc = new Document("Doc");
        AssertRaises(doc.publish, ..., NotApprovedException);
    }
    
  • ドキュメントを再発行することはできません。オブジェクトを承認するテストを記述し、成功するとパブリッシュを呼び出しますが、2回目にはオブジェクトの状態を変更せずに正しいエラーが発生します。

    void testRePublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        AssertRaises(doc.publish, ..., RepublishException);
    }
    
  • 発行されると、PublishedByおよびPublishedOnの値が適切に設定されます。承認を呼び出してから発行を呼び出すテストを作成し、オブジェクトの状態が正しく変更されることをアサートします

    void testPublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        Assert(doc.PublishedBy, ...);
        ...
    }
    
  • 公開されると、PublishedEventが発生します。イベントシステムにフックし、呼び出されるようにフラグを設定します

また、承認のためのテストを作成する必要があります。

つまり、内部フィールドとIsPublishedおよびIsApprovedの関係をテストしないでください。フィールドを変更するとテストコードが変更されるため、テストは非常に無意味になります。代わりに、テストを変更する必要がないフィールドを変更した場合でも、この方法でパブリックメソッドの呼び出し間の関係をテストする必要があります。


承認が中断すると、いくつかのテストが中断します。コードの単位をテストするのではなく、完全な実装をテストするのです。
pdr

私はpdrの懸念を共有しており、それがこの方向に進むことをheした理由です。はい、それは最もきれいに見えますが、個々のテストが失敗する可能性がある複数の理由があるのは好きではありません。
-SonOfPirate

4
単一の考えられる理由でのみ失敗する可能性のある単体テストはまだ見ていません。また、テストの「状態操作」部分をsetup()、テスト自体ではなくメソッドに入れることができます。
ピーターK.

12
なぜapprove()脆いのに依存してsetApproved(true)いるのに、なぜそうではないのに依存しているのですか? approve()要件の依存関係であるため、テストの正当な依存関係です。依存関係がテストにのみ存在する場合、それは別の問題になります。
カールビーレフェルト

2
@pdr、スタッククラスをどのようにテストしますか?メソッドpush()pop()メソッドを個別にテストしてみますか?
ウィンストンイーバート

2

さらに別のアプローチは、インスタンス化時に内部プロパティを設定できるクラスのコンストラクターを作成することです。

 public Document(
  String approvedBy,
  DateTime? approvedOn,
  Boolean isApproved,
  Boolean isPublished,
  String publishedBy,
  DateTime? publishedOn,
  String title)
{
  ApprovedBy = approvedBy;
  ApprovedOn = approvedOn;
  IsApproved = isApproved;
  IsApproved = isApproved;
  PublishedBy = publishedBy;
  PublishedOn = publishedOn;
}

2
これは、まったくうまくスケーリングしません。私のオブジェクトには、オブジェクトのライフサイクルの任意の時点で値を持っているか持っていないプロパティがいくつもあり、さらに多くのプロパティを持つことができます。オブジェクトが有効な初期状態またはオブジェクトが機能するために必要な依存関係にあるために必要なプロパティのパラメーターがコンストラクターに含まれているという原則に従います。この例のプロパティの目的は、オブジェクトが操作されるときに現在の状態をキャプチャすることです。すべてのプロパティを持つコンストラクタまたは異なる組み合わせのオーバーロードを持つことは大きな臭いであり、私が言ったように、スケールしません。
-SonOfPirate

わかった。あなたの例では、これ以上多くのプロパティについて言及していませんでした。例の数は、これを有効なアプローチとして持つ「カスプに」です。あなたのデザインについて何かを語っているようです:あなたがすることはできませんにあなたのオブジェクトを置く任意のインスタンス化に有効な状態。つまり、有効な初期状態にする必要があり、テストのために適切な状態に操作します。それは、リー・ライアンの答えが進むべき道であることを意味します
ピーターK.

オブジェクトに1つのプロパティがあり、変更されない場合でも、この解決策は不適切です。実稼働環境でこのコンストラクターを使用できないのはなぜですか?このコンストラクターをどのようにマークしますか[TestOnly]?
ピョートルペラ

本番環境でなぜ悪いのですか?(本当に、私は知りたいです)。単一の有効な初期オブジェクトではなく、作成時にオブジェクトの正確な状態を再作成する必要がある場合があります。
ピーターK.

1
そのため、オブジェクトを有効な初期状態にするのに役立ちますが、ライフサイクルを通じてオブジェクトの動作をテストするには、オブジェクトを初期状態から変更する必要があります。私のOPは、オブジェクトの状態を変更するためのプロパティを単に設定できない場合に、これらの追加の状態をテストすることに関係しています。
SonOfPirate

1

1つの戦略は、クラス(この場合はDocument)を継承し、継承されたクラスに対してテストを作成することです。継承されたクラスにより、テストでオブジェクトの状態を設定する何らかの方法が可能になります。

C#での1つの戦略は、セッターを内部にし、内部をテストプロジェクトに公開することです。

説明したようにクラスAPIを使用することもできます(「Publishを2回呼び出すことでこれを実行できます」)。これは、オブジェクトのパブリックサービスを使用してオブジェクトの状態を設定することになるため、私にはあまり臭いがしません。あなたの例の場合、これはおそらく私がやる方法でしょう。


これを解決策として考えましたが、オブジェクトを開いてカプセル化を解除しているように感じたため、プロパティをオーバーライド可能にするか、セッターを保護対象として公開することをためらいました。プロパティを保護することは、パブリックまたは社内/友人よりも確かに優れていると思います。私は間違いなくこのアプローチにもっと考えを与えます。シンプルで効果的です。時にはそれが最善のアプローチです。誰も同意しない場合は、詳細を記載したコメントを追加してください。
-SonOfPirate

1

ドメインオブジェクトが受け取るコマンドとクエリを完全に分離しテストするために、各テストに期待される状態のオブジェクトのシリアル化を提供するために使用されます。テストのarrangeセクションでは、事前に準備したファイルからテストするオブジェクトをロードします。最初はバイナリシリアル化から始めましたが、jsonの方がずっと簡単に管理できることがわかっています。これは、テストでの絶対的な分離が実際の価値を提供するときはいつでも、うまく機能することが証明されました。

メモを編集するだけで、JSONのシリアル化が失敗する場合があります(循環オブジェクトのグラフの場合、臭いがするなど)。このような状況では、バイナリシリアル化を利用します。少し実用的ですが、動作します。:-)


そして、セッターがなく、それを設定するためにパブリックメソッドを呼び出したくない場合、どのようにオブジェクトを期待される状態に準備しますか?
ピョートルペラ

そのための小さなツールを作成しました。リフレクションによってクラスをロードし、パブリックコンストラクター(通常は識別子のみを使用)を使用して新しいエンティティを作成し、その上でAction <TEntity>の配列を呼び出し、各操作の後にスナップショットを保存します(アクションのインデックスに基づく従来の名前で)およびエンティティ名)。このツールは、エンティティコードのリファクタリングごとに手動で実行され、スナップショットはDCVSによって追跡されます。明らかに、各アクションは、エンティティの公開コマンドを呼び出しますが、これは、この方法が本当にあるのテストランのうちに行われているユニットテスト。
ジャコモテシオ

私はそれが何を変えるか理解していません。それでもsut(テスト対象システム)でパブリックメソッドを呼び出す場合は、テストでそれらのメソッドを呼び出す場合と同じです。
ピョートルペラ

スナップショットが作成された後、それらはファイルに保存されます。各テストは、エンティティの開始状態を取得するために必要な操作のシーケンスに依存するのではなく、状態自体(スナップショットからロード)から取得します。テスト対象のメソッド自体は、他のメソッドへの変更から分離されます。
ジャコモテシオ

テスト用にシリアル化された状態を準備するために使用されたパブリックメソッドを変更したが、シリアル化されたオブジェクトを再生成するツールの実行を忘れた場合はどうなりますか?コードにエラーがある場合でも、テストは緑色のままです。それでも、これは何も変わらないと言います。まだパブリックメソッドを実行しているので、テストするオブジェクトをセットアップします。ただし、テストを実行するずっと前に実行します。
ピョートルペラ

-7

あなたは言う

可能な限りベストプラクティスを適用するようにしてください

そして

オブジェクトをハイドレートするために使用しているORMはリフレクションを使用するため、プライベートセッターにアクセスできます。

また、リフレクションを使用してクラスのアクセス制御をバイパスすることは、「ベストプラクティス」とは言いません。その速度も恐ろしく遅くなります。


個人的には、ユニットテストフレームワークを破棄し、クラス内の何かに取り組みます。とにかくクラス全体をテストするという観点からテストを書いているようです。過去には、テストが必要ないくつかのトリッキーなコンポーネントについては、アサートとセットアップコードをクラス自体に埋め込みました(これは、すべてのクラスにtest()メソッドを持つ一般的なデザインパターンでした)ので、クライアントを作成しますこれは単にオブジェクトをインスタンス化し、リフレクションハックのような厄介なことなく自分で好きなようにセットアップできるテストメソッドを呼び出します。

コードが肥大化している場合は、テストコードを#ifdefsでラップして、デバッグコードでのみ使用できるようにします(おそらくベストプラクティス自体)


4
-1:テストフレームワークを廃止し、クラス内のテストメソッドに戻ると、ユニットテストの暗い時代に戻ります。
ロバートジョンソン

9
私からの-1はありませんが、実稼働環境にテストコードを含めることは一般的にBad Thing(TM)です。
ピーターK.

OPは他に何をしますか?プライベートセッターでねじ込むことに固執しますか?!あなたが飲みたい毒を選ぶようなものです。OPに対する私の提案は、ユニットテストを実稼働環境ではなくデバッグコードに組み込むことでした。私の経験では、単体テストを別のプロジェクトに入れるということは、プロジェクトがとにかく元のプロジェクトと密接に結びつくことを意味するため、dev PoVからはほとんど区別がありません。
gbjbaanb
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.