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


8

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

myOrder.updateQuantity(2);

実際に、第三者にメッセージを送信するためには、第三者にも同様THISために固有のいくつかのことを、知っている必要がありorderID、かつloginID、アプリケーションの存続期間中に変化しないであろう。

私が作成したときにmyOrder、もともと、私は注射MessageFactory知っています、loginID。次に、updateQuantityが呼び出されると、Orderパスがを通過しorderIDます。制御コードは簡単に記述できます。別のスレッドがコールバックを処理し、Order変更が成功した場合は更新Orderし、変更が失敗した場合は変更が失敗したことを通知します。

問題はテストです。のでOrder、オブジェクトが依存するMessageFactory、それが必要MessageFactory実際返すようにMessageSを(それが呼び出すこと.setOrderID()例えば、上)、今私は非常に複雑に設定する必要が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);
    }
}

Orderオブジェクトとに対して作成した関連コードを表示する必要があると思いますMessageFactory。これは良い説明ですが、明確な回答で直接取り組むのは少し抽象的です。
ロバートハーベイ

@RobertHarvey更新が役立つことを願っています。
durron597 2014年

layer.sendメッセージを送信すること、または正しいメッセージを送信することを確認しようとしていますか?
ロバートハーベイ、

@RobertHarvey私は、呼び出しを考えていたverify(messageMock).setQuantity(2)、とverify(layer).send(messageMock);。またupdateQuantity場合はfalseを返す必要がありOrder、既に保留中の更新を持っていますが、私はsscce上の理由からそのコードを省略しました。
durron597 2014年

3
私はあなたのコードをもう少し見る必要があります...しかし、記録のために、モックを返すことはそれほど大きな問題であるとは考えていません。モックベースのテストの複雑さは、可変オブジェクトを処理することの避けられない結果であり、モックはあなたの最終的にリリースされたアプリケーションに影響を与えないので、返すものが子猫を殺す方法はわかりません。
Robert Harvey

回答:


12

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

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

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

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

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


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

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

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

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


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

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

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

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


+1コンストラクタについて完全に同意します。NullMessageを返すことはMockを返すことよりも優れているかどうかはわかりません-専門性を間違えているようです。いずれにせよ、結果は「偽物」です。
Rob

Lol私はGuavaの情報源から「自分のビルダーを引数として取る」トリックを盗みました。
durron597 2014年

@RobY nullオブジェクトは、正当なopとしてアプリケーションの一部として使用する場合、偽物である必要はありません。ただし、それが実際にテスト用の偽物としてのみ使用されている場合は、別のタイプの実数Messageを使用する必要があります。
cbojar 2014年

@cbojar許可されていますが、null可能な参照を処理するためのより良い方法としてOptional / Maybeが登場しています。単体テスト用にNullObjectのみを記述している場合、NullObjectは、昔のように、実際には手書きのモックにすぎません。問題は、「モックからのモックなし」ルールを明確に正当化しないと、主観的なスタイルのルールに違反するかどうかにかかわらず、NullObjectが悪いかどうか、または理由を判断するのが難しいことです。
Rob

ところで、実際のMessage実装はサードパーティのAPIに依存しています。開始する必要があるクラスのインスタンスを作成できるようにするためEngineには、とりわけ、ライセンスキーが必要です...単体テストで必要なものとは異なります。
durron597 2014年

2

@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);

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


流暢なインターフェイスは同じオブジェクトを返すため、流暢なインターフェイスはDemeterに引き続き従うことができます。つまり、はとobj.law().of().demeter();同じobj.law(); obj.of(); obj.demeter();です。これは完全に許容可能です。プロトタイプについても同様の説明ができます。
cbojar 2014年

はい。ただし、実際に機能し、それらをモックアウトしたい場合は、「モックからモックを作成しない」ルールに反対します。つまり、それらを使用するコードはテストできなくなります。
Rob

@cbojarああ、私は明確にする必要があります。デメテルは合理的に聞こえ、良いプログラミングスタイルです。一般的に適用可能な(そして正当化された)プラクティスではなく、特定のコーディングスタイルをサポートする主観的な好みのように私を襲うのは「モックからのモックなし」です。
Rob

ちなみに、私はコードの別の部分で流暢なインターフェイスを使用しています。私からいくつかのアイデアを持って、このブログの記事、そしてここで実際のコードのIの使用である:pastebin.com/D1GxSPsy
durron597

それは賢いです。涼しい!私が今取り組んでいるコードで試してみます。ただ一つのメモ-流暢なインターフェースはそれ自体を返す必要はありません。Fluentインターフェイスが一連の不変のインスタンスを返すバリエーションがあります。つまり、「return this」の代わりに「return new Fluent(...)」を使用します。ただし、これはクライアントに対して透過的であるため、ここでの説明には影響しません。しかし、それは一種の面白い事実です。
Rob
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.