2009-11-19 10 views
7

最近、テストでモックオブジェクト(Javaのmockitoを使用)を使用し始めました。言うまでもなく、彼らはテストのセットアップ部分を簡素化し、Dependency Injectionとともにコードをより堅牢にしたと主張します。テストでのモックの使用

しかし、私は自分が仕様ではなく実装に対してテストを行っていることがわかりました。私はそれがテストの一部ではないと主張するという期待を設定しました。より専門的に言えば、私はSUT(テスト対象のクラス)とその共同研究者との間の相互作用をテストすることになります。そのような依存関係は、契約やクラスのインタフェースの一部ではありません!

XMLノードを扱うとき、ノードの属性値を返すメソッドattributeWithDefault()があるとします。そうでない場合は、デフォルト値が返されます。

私は次のようなセットアップのテストをします:

Element e = mock(Element.class); 

when(e.getAttribute("attribute")).thenReturn("what"); 
when(e.getAttribute("other")).thenReturn(null); 

assertEquals(attributeWithDefault(e, "attribute", "default"), "what"); 
assertEquals(attributeWithDefault(e, "other", "default"), "default"); 

さて、ここだけではなく、私は、私がElement.getAttribute()を使用するためにそれを必要に応じて、attributeWithDefault()が仕様に準拠して、私はまた、実装をテストしたことをテストしましたElement.getAttributeNode().getValue()またはElement.getAttributes().getNamedItem().getNodeValue()の代わりに

私はそれを間違った方法で行っていると思いますので、モックの使い方とベストプラクティスを向上させる方法についてのヒントがあれば幸いです。

編集:私はテストが悪いのスタイルであるという上記の仮定をしたテスト

と間違って何 、ここに私の根拠はあります。

  1. この仕様では、どのメソッドが呼び出されるかは指定されていません。ライブラリのクライアントは、正しく実行されている限り、属性がどのように取得されたかを気にする必要はありません。実装者は、(パフォーマンス、一貫性などに関して)どのような方法であれ、代替アプローチにアクセスするための自由な治世を持つべきである。 Elementの仕様では、これらのアプローチがすべて同じ値を返すようにしています。

  2. ElementgetElement()という1つのメソッドインタフェースに再因子付けすることは意味がありません。使い易さのために、メソッドのクライアントは標準ライブラリの標準Elementを使用するだけです。インターフェイスと新しいクラスを持つことは、クライアントのコードを醜いものにするので、単なる愚かなIMHOです。それは価値がありません。

  3. スペックはそのままで、テストはそのままであると仮定すると、新しい開発者はコードをリファクタリングして状態を使用する別のアプローチを使用し、テストを失敗させる可能性があります。実際の実装が仕様に準拠していない場合、テストは失敗します。

  4. 共同編集者を複数の形式で公開することはかなり一般的です。仕様とテストは、どのようなアプローチが取られているかに依存すべきではありません。実装だけが必要です!

答えて

6

これは、モックテストで共通の問題であり、離れてこのから取得するための一般的なマントラです:

Only mock types you own。ここで

あなたは(小さなテストXMLは、ユニットのコンテキスト内でうまく動作するはずのように、必ずしも、正直、必要ありません)XMLパーサとのコラボレーションをモックとしたい場合は、そのXMLパーサーはそのインターフェイスまたはクラスの後ろにする必要がありますコールする必要があるサードパーティAPIのどのメソッドの面倒な詳細を処理するかをあなた自身が所有しています。要点は、要素から属性を取得するメソッドがあることです。その方法を模倣する。これは実装を設計から分離します。実際の実装では実際のユニットテストが行​​われ、実際のオブジェクトから正常な要素を取得することが実際にテストされます。

モックは基本的にスタブとして機能するボイラープレートのセットアップコードを保存するのに適していますが、それは駆動設計の主な目的ではありません。モックはテスト動作で(状態とは対照的に)、not Stubsです。

Mockをスタブとして使用すると、コードのように見えます。どんなスタブも、あなたの実装に縛られているそれをどのように呼び出すかについて仮定をしなければなりません。それは正常です。それが問題なのは、それがあなたのデザインを悪い方法で動かしているかどうかです。

+1

+1。動作(モック)と状態(スタブ)テストの違いを明確にしたように。 – notnoop

0

私はここであなたを見ることができます(と私はあなたが使用しているライブラリに慣れていないんだ認めなければならない)唯一の解決策は、含まれる機能のすべてを持っているモック要素を作成することですgetAttributeNote()。getValue()およびgetAttributes()。getNamedItem()。getNodeValue()の値を設定する機能もあります。

しかし、それらがすべて同等であるとすると、テストするだけで問題ありません。すべてのケースをテストする必要があることはさまざまです。

1

ユニットテストを設計するときは、抽象的な仕様ではなく、実装を常に効果的にテストします。または、技術仕様の拡張されたビジネス仕様である「技術仕様」をテストすると主張できます。これには何も問題はありません。テストする代わりに:

私のメソッドは、定義されている場合、またはデフォルトの値を返します。

あなたがテストしている:

を定義した場合私のメソッドは値を返しますまたはデフォルトは、私がのgetAttribute(名前)を呼び出したときに供給XML要素は、この属性を返すことを提供します。

+0

あなたは何がテストされているかを把握しています。しかし、私は、この方法の公的契約はそれほど厳しくすべきではないと主張します。 – notnoop

0

モックの使用に間違いはありません。あなたがテストしているのは、Elementが正しいかどうかではなく、attributeWithDefault()メソッドとその実装です。だからあなたは必要なセットアップの量を減らすためにElementを嘲笑した。このテストでは、attributeWithDefault()の実装が仕様に適合していることが保証されていますが、当然テスト用に実行できる特定の実装が必要です。

0

ここでモックオブジェクトを効果的にテストしています。 attributeWithDefault()メソッドをテストする場合は、e.getAttribute()が期待される引数で呼び出され、戻り値を忘れることをアサートする必要があります。この戻り値は、モックオブジェクトの設定のみを検証します。 (これはJavaのmockitoでどのように正確に行われたのか分かりませんが、私は純粋なC#の人です...)

+1

テストでは、テスト対象のクラスが 'e.getAttribute()'の戻り値に応じて動作を変更することを検証しているようです。モックが正しい値を返すかどうかをテストするのではなく、クラスの振る舞いが戻り値に基づいて変更されることを確認します。 – Yishai

0

getAttribute()を呼び出して属性を取得するかどうかは、仕様の一部であるか、または変更の可能性のある実装の詳細であるかによって異なります。

要素がインターフェイスである場合、属性を取得するために 'getAttribute'を使用する必要があるとするのはおそらくインターフェイスの一部です。だからあなたのテストは大丈夫です。

Elementが具象クラスの場合、attributeWithDefaultは、ここに表示されるのを待っているインタフェースよりも、属性の取得方法を認識すべきではありません。

public interface AttributeProvider { 
    // Might return null 
    public String getAttribute(String name); 
} 

public class Element implements AttributeProvider { 
    public String getAttribute(String name) { 
     return getAttributeHolder().doSomethingReallyTricky().toString(); 
    } 
} 

public class Whatever { 
    public String attributeWithDefault(AttributeProvider p, String name, String default) { 
    String res = p.getAtribute(name); 
    if (res == null) { 
     return default; 
    } 
    } 
} 

次に、ElementではなくMock AttributeProviderに対してattributeWithDefaultをテストします。

もちろん、このような状況ではおそらく過度のテストになるでしょうし、実装してもテストはうまくいくでしょう(とにかくどこかでテストしなければなりません))。しかし、このようなデカップリングは、論理がgetAttributeまたはattributeWithDefualtのいずれかにより複雑になる場合に役立ちます。

これが役に立ちます。

+0

私はそれが過度なことに同意します。私は投稿を更新しました。 – notnoop

+0

「getElementを使用して単一のインタフェース」と言います。あなたは「getAttributeを持つ単一のインターフェース」を意味しましたか? 更新後:正しく理解していれば、attributeWithDefaultを実際に要素から取得する方法を実際に知ることを防ぐ必要があります。私はそれを理解しており、これを強制するもう1つの方法は、間接参照の別のレベルを追加することです(Elementを渡す代わりに、要素から属性を取得する方法を知っている別のオブジェクトを渡します)。しかし、どういうことであろうと、要素から属性を取得する方法を知っているクラスが必要になると思います。 – phtrivier

0

あなたがこの方法で検証する3つの事があるように私には思える:属性がない場合には

  • それが正しい場所(Element.getAttribute())から属性を取得し

    1. は、 nullの場合、それは
    2. 属性がnullの場合は返送され、文字列 "デフォルトは" あなたは現在#2、#3ではなく、#1を検証している

    が返されます。 mockitoを使用すると、メソッドが実際にあなたのモックで呼び出さなっていることを保証

    verify(e.getAttribute("attribute")); 
    verify(e.getAttribute("other")); 
    

    を追加することにより、#1を確認することができます。確かに、これはmockitoで少しclunkyです。

    expect(e.getAttribute("attribute")).andReturn("what"); 
    expect(e.getAttribute("default")).andReturn(null); 
    

    それは同じ効果がありますが、私は読んで、あなたのテストは少し簡単になり考える:easymockでは、あなたのような何かをしたいです。

  • +0

    @chrispix。属性を取得するための3つの「適切な場所」があります。私が書いたテストは、そのうちの1つが使われていることを確認するだけです。実装は自由に選択できます。テストは、どこで検索されるかは気にしないでください。 – notnoop

    +0

    あなたのクラスがその依存関係とどのように協力し合っているかについて、テストに無関係にしたい場合は、依存関係を模擬すべきではありません。テストXMLスニペットを使用してください。 クラスの複雑さによっては、現実的ではない可能性があります。テストXML(または他の構造化された)データを管理することは大きな苦痛を与え、テストを脆弱にする可能性があります。 –

    +0

    誰かが実装をリファクタリングしたときにテストが壊れると、大きな問題になるとは思わない。異なるメソッドを使用するリファクタリングは有効かもしれませんが、テストを更新するだけでも簡単です。 その場合、失敗するテストは、リファクタリングが実際に有効であることを確認する警告として機能します。私は開発者に、仕様に準拠していると思うような実装をリファクタリングしていましたが、間違っていました。より良いテストが間違いを露呈させたでしょう。 –

    0

    依存関係注入を使用している場合、共同作業者は契約の一部である必要があります。すべての共同作業者をコンストラクタまたはパブリックプロパティを通じて注入する必要があります。

    ボトムライン:注入する代わりに新しく作成した共同作業者がいる場合は、おそらくコードをリファクタリングする必要があります。これは、テスト/擬似/注入に必要なマインドセットの変更です。

    +0

    また、特定のコード例を見ると、あなたのメソッドに 'Element e'がどのように渡されているかを指定していません。それはオブジェクトに注入されるか、パラメータとして渡されますか?この場合は、モックを使用するのではなく、テストケースにマッチするようにデータを設定した具体的なオブジェクトを使用する方が意味があります。 – Brett

    +0

    「DI」を採用するとき、どのメソッドを呼び出すかを明示的に指定する必要があることを意味しますか?仕様書のリストが空でないときは、実際には 'list.size()!= 0'ではなく'!list.isEmpty() 'を呼び出すことを明確にする必要があります。それは契約を汚染しないだろうか? – notnoop

    +0

    @Brett、私はそれが静的メソッドであると仮定しています。 – notnoop

    0

    これは遅れての回答ですが、他の見解とは異なる視点を取ります。

    基本的に、OPは、質問に記載された理由から、嘲笑のテストが悪いと考えるのは正しいです。モックが大丈夫だと言っている人は、それには十分な理由がありません。

    の2種類のテストがあります.1つは模擬(BAD)と、もう1つは不適合(GOOD)です。 (私は別の模倣ライブラリを使用する自由を取ったが、それはその点を変えない。)

    import javax.xml.parsers.*; 
    import org.w3c.dom.*; 
    import org.junit.*; 
    import static org.junit.Assert.*; 
    import mockit.*; 
    
    public final class XmlTest 
    { 
        // The code under test, embedded here for convenience. 
        public static final class XmlReader 
        { 
         public String attributeWithDefault(
          Element xmlElement, String attributeName, String defaultValue 
         ) { 
          String attributeValue = xmlElement.getAttribute(attributeName); 
          return attributeValue == null || attributeValue.isEmpty() ? 
           defaultValue : attributeValue; 
         } 
        } 
    
        @Tested XmlReader xmlReader; 
    
        // This test is bad because: 
        // 1) it depends on HOW the method under test is implemented 
        // (specifically, that it calls Element#getAttribute and not some other method 
        //  such as Element#getAttributeNode) - it's therefore refactoring-UNSAFE; 
        // 2) it depends on the use of a mocking API, always a complex beast which takes 
        // time to master; 
        // 3) use of mocking can easily end up in mock behavior that is not real, as 
        // actually occurred here (specifically, the test records Element#getAttribute 
        // as returning null, which it would never return according to its API 
        // documentation - instead, an empty string would be returned). 
        @Test 
        public void readAttributeWithDefault_BAD_version(@Mocked final Element e) { 
         new Expectations() {{ 
          e.getAttribute("attribute"); result = "what"; 
    
          // This is a bug in the test (and in the CUT), since Element#getAttribute 
          // never returns null for real. 
          e.getAttribute("other"); result = null; 
         }}; 
    
         String actualValue = xmlReader.attributeWithDefault(e, "attribute", "default"); 
         String defaultValue = xmlReader.attributeWithDefault(e, "other", "default"); 
    
         assertEquals(actualValue, "what"); 
         assertEquals(defaultValue, "default"); 
        } 
    
        // This test is better because: 
        // 1) it does not depend on how the method under test is implemented, being 
        // refactoring-SAFE; 
        // 2) it does not require mastery of a mocking API and its inevitable intricacies; 
        // 3) it depends only on reusable test code which is fully under the control of the 
        // developer(s). 
        @Test 
        public void readAttributeWithDefault_GOOD_version() { 
         Element e = getXmlElementWithAttribute("what"); 
    
         String actualValue = xmlReader.attributeWithDefault(e, "attribute", "default"); 
         String defaultValue = xmlReader.attributeWithDefault(e, "other", "default"); 
    
         assertEquals(actualValue, "what"); 
         assertEquals(defaultValue, "default"); 
        } 
    
        // Creates a suitable XML document, or reads one from an XML file/string; 
        // either way, in practice this code would be reused in several tests. 
        Element getXmlElementWithAttribute(String attributeValue) { 
         DocumentBuilder dom; 
         try { dom = DocumentBuilderFactory.newInstance().newDocumentBuilder(); } 
         catch (ParserConfigurationException e) { throw new RuntimeException(e); } 
         Element e = dom.newDocument().createElement("tag"); 
         e.setAttribute("attribute", attributeValue); 
         return e; 
        } 
    } 
    
    関連する問題