web-dev-qa-db-ja.com

ユニットテスト、工場、そしてデメテルの法則

これが私のコードの仕組みです。サードパーティのショッピングAPIに保存されている、ショッピングカートの注文に似たものの現在の状態を表すオブジェクトがあります。私のコントローラーコードで、私は呼び出すことができるようにしたいです:

_myOrder.updateQuantity(2);
_

メッセージを実際にサードパーティに送信するために、サードパーティは、orderIDloginIDなど、この注文に固有のいくつかのことを知っている必要があります。これらは、アプリケーションの存続期間中に変更されません。

したがって、最初にmyOrderを作成するときに、MessageFactoryを認識しているloginIDを挿入します。次に、updateQuantityが呼び出されると、OrderorderIDを渡します。制御コードは簡単に記述できます。別のスレッドがコールバックを処理し、変更が成功した場合はOrderを更新し、変更が失敗した場合はOrderに通知します。

問題はテストです。 OrderオブジェクトはMessageFactoryに依存しており、実際のMessageFactorys(たとえば、.setOrderID()を呼び出す)を返すにはMessageが必要であるため、非常に複雑なMessageFactoryモックを設定する必要があります。 さらに、「モックがモックを返すたびに妖精が死ぬ」ので、私は妖精を殺したくありません

コントローラーのコードを単純に保ちながら、この問題を解決するにはどうすればよいですか?私はこの質問を読みました: https://stackoverflow.com/questions/791940/law-of-demeter-on-factory-pattern-and-dependency-injection しかし、それは役に立たなかったので助けにはなりませんでしたテストの問題について話しません。

私が考えたいくつかの解決策:

  1. どういうわけか、ファクトリメソッドが実際のオブジェクトを返すことを要求しないようにコードをリファクタリングします。おそらくそれはファクトリーではなく、MessageSenderのほうが多いのでしょうか?
  2. MessageFactoryのテスト専用の実装を作成し、それを注入します。

コードはかなり複雑です。sscceでの私の試みは次のとおりです。

_public class Order implements UpdateHandler {
    private final MessageFactory factory;
    private final MessageLayer layer;

    private OrderData data;

    // Package private constructor, this should only be called by the OrderBuilder object.
    Order(OrderBuilder builder, OrderData initial) {
        this.factory = builder.getFactory();
        this.layer = builder.getLayer();
        this.data = original;
    }

    // Lots of methods like this
    public String getItemID() {
        return data.getItemID();
    }

    // Returns true if the message was placed in the outgoing network queue successfully. Doesn't block for receipt, though.
    public boolean updateQuantity(int newQuantity) {
        Message newMessage = factory.createOrderModification(messageInfo);

        // *** THIS IS THE KEY LINE ***
        // throws an NPE if factory is a mock.
        newMessage.setQuantity(newQuantity); 

        return layer.send(newMessage); 
    }

    // from interface UpdateHandler
    // gets called asynchronously
    @Override 
    public handleUpdate(OrderUpdate update) {
        messageInfo.handleUpdate(update);
    }
}
_
8
durron597

ここでの主な懸念は、モックがモックを返すことができない(またはすべきでない)ことです。これはおそらく良いアドバイスですが、解決策について話します:実際のMessageを返します。 Messageクラスが十分にテストされて合格している場合は、モックと同じくらいフレンドリーであると考えることができます。実物だから本物と同じように反応してくれるのでもっと親しみやすいかもしれません。

実際にどのようなMessagesを返すことができますか?さて、本格的な実際のMessage、簡略化された実際のMessage(よく知られたデフォルトが使用されている)、またはNullMessageを返すことができます(Nullオブジェクトパターンの場合と同様)。 NullMessageは、他のMessageと同様に有効であり、アプリケーションの他の場所にドロップできます。どちらを使用するかは、メッセージ全体を作成して返す複雑さによって異なります。

デメテルの法則に関して、ここには複数の懸念があります。最初に、コンストラクターは独自のビルダーをパラメーターとして取り、それから要素を抽出します。これはDemeterの明らかな違反であり、余分な依存関係も作成します。さらに悪いことに、ビルダーはミニサービスロケーターとして機能し、クラスの実際の依存関係をマスクします。 OrderBuilderは、これらのオブジェクトを作成し、独自のパラメーターとして渡す必要があります。

これをテストするには、実際のMessageFactory(完全、単純、またはnullのいずれか)を返すモックMessageと、メッセージを受け取るモックMessageLayerを渡します。完全または簡略化されたMessageを使用する場合、MessageLayerモックから取得して、アサーションのテストのために検査することができます。

また、MessageFactoryMessageLayerを異なる抽象化レベルの機能のまとまりとして見て、その機能をカプセル化したMessageSenderクラスを抽出します。単純なモックMessageSenderを使用してこのクラスをテストし、上記で説明したすべてをMessageSenderのテストにシフトすることで、単一の責任にも厳密に準拠できます。


ここには本当に2つの質問があるようです。 thisコード、、およびをテストする方法に関する特定の質問があり、モックを返すモックに関する一般的な質問があります。具体的な質問は、私が上記で大々的に扱ったものであり、いくつかの詳細が明らかになったので、ここで最後にもっと考えましたが、一般的な質問にはまだ良い答えはありません:モックがモックを返さないのはなぜですか?

モックがモックを返すべきではない理由は、コードをテストするのではなく、テストをテストしてしまう可能性があるためです。ユニットが完全に機能していることを確認するだけでなく、テストはテストケース自体(多くの場合、それ自体はテストされない)でのみ見つかるまったく新しいコードに依存します。これには2つの問題があります。

まず、テストでは、ユニットが壊れているかどうか、または相互に関連するモックが壊れているかどうかを確認できません。テストの要点は、障害の原因が1つだけである隔離された環境を作成することです。モック自体は一般に非常に単純で、問題を直接検査できますが、このように複数のモックを一緒に配線すると、検査で指数関数的に確認することが難しくなります。

2番目の問題は、実際のオブジェクトのAPIが変更されると、モックも自動的に変更されないため、テストがはるかに失敗する可能性があることです。デメテルの法則はここで効力を発揮します。これらは法則が回避する影響のタイプであるためです。私のテストでは、直接の依存関係のモックだけでなく、依存関係の依存関係のモックad infinitumも同期することを心配する必要があります。これは、クラスが変更された場合のテストに対するショットガン手術の効果があります。


ここで、この特定のコードをテストする方法に関する特定の質問について、いくつかの仮定を分解してみましょう。

質問1:実際に何をテストしていますか?これはコードの省略部分ですが、ここでは3つの重要なアクティビティが行われていることがわかります。まず、Messageを生成するファクトリがあります。ファクトリーがMessageを生成しているかどうかはテストしていません。すでにモックアウトしているからです。 Messageは、おそらくMessageを生成するサードパーティAPIのテストスイートで他の場所でテストする必要があるため、テストしていません。 2行目では、検査からメソッドが単にMessageで呼び出され、2行目でテストするものは何もないことがわかります。この場合も、テストを冗長にする他の場所でテストを行う必要があります。 3行目はMessageLayer APIを呼び出し、単純に結果を渡します。この場合も、MessageLayerのAPIは別の場所でテスト済みです。これにより、基本的にテストするものは何もありません。外部コードに直接目に見える副作用はなく、内部実装をテストするべきではありません。 このコードをテストするのは不適切であるという結論に至ります。 (この推論の行について詳しくは、Sandi Metzのプレゼンテーションを参照してくださいテストの魔法のトリック、[ slidesvideo ])

質問2:待って、それで...何ですか?はい、そうです、これをまったくテストしないでください。さて、前述のように、これはコードの省略バージョンです。他のロジックがある場合はテストしますが、これを別のユニットにカプセル化します(上記のMessageSender実装など)。その後、他のロジックをテストする機能を維持しながら、コードのこの側面全体を簡単にモックできます。

基本的に、コードで直接サードパーティのAPIを使用しています。サードパーティのコードは、この種の依存関係の問題が発生する可能性があるため、テストが難しいことで有名です。囲いのある領域にカプセル化すると、他のコードのテストが容易になり、サードパーティがコードを変更した場合(または単に変更した場合)にショットガンの手術を減らすことができます。サードパーティのAPIとやり取りする部分をテストするのはまだ難しいかもしれませんが、分離できるのは1つの小さなファセットに限定されます。

12
cbojar

@Robert Harveyに同意します。明確にするために:Demeterは合理的で優れたプログラミングスタイルです。一般的に適用可能な(そして正当化された)プラクティスではなく、特定のコーディングスタイルをサポートする主観的な好みのように私を襲うのは、「モックからのモックなし」です。フェアリールールは、次のような「流暢な」インターフェースを取り出します。

Thing.create( "zoom")。setDomain( "bo.com")。add(1).flip()。reverse()。tuneForSemantics()。run();

極端な例のようなものですが、コードがテストできなくなるため、本質的にフェアリールールはそのクラスをコードに含めることを許可しません。しかし、これはOOコードでよく使われるパラダイムです。

また、より一般的な問題は、ファクトリをモックして、テストしたいモックを返す方法です。私は通常、ファクトリーを依存関係として使用するのを恥ずかしがっていますが、代替手段よりもはるかに優れている場合があります。あなたがで終わる場合

ThirdPartyThing ThirdPartyFactory<ThirdPartyThing>#create()

どうすればそれを回避できるかわかりません。モックを返すにはモックが必要です。したがって、このルールは、2つの非常に強力なOOデザインパターンを打ち消します。

そのメソッドを2または3に分割して、長い行をクライアントにプッシュするか、または奇妙なステートフルラッパークラスを作成せずに問題を回避する方法を考えることはできません。

私は代替案がどのように見えるかを見て本当に興味があります。

私の答え:あなたのコードは大丈夫です!エクセルシオール!

(実際、私は代替案に興味があります)

...

境界のケースを考えてみましょう:モックは自分自身を返すことを許可されていますか?技術的には、彼らはモックを返しています。そうでない場合は、GoFプロトタイプがノックアウトされます。これは、長い間続いてきたパターンの1つです。

MuActor prototype = ...
...
MuActor actor = prototype.create();
actor.run();

ルールも許可します:

prototype = Mock(MuActor.class);
when(prototype.create()).thenReturn(prototype);

また、妖精のルールはモナドの使用をほとんど禁止しています。モナドは特定のコンテナタイプの操作チェーンに基づいているためです。 Monadタイプをテストすることはできますが、Monadが表示されるコードをテストすることはできません。

2
Rob