簡體   English   中英

在測試中使用模擬

[英]Use of Mocks in Tests

我剛剛開始在我的測試中使用模擬對象(使用Java的mockito)。 毋庸置疑,他們簡化了測試的設置部分,並且隨着依賴注入,我認為它使代碼更加健壯。

但是,我發現自己在實施而不是規范的測試中絆倒。 我最終建立了期望,我認為這不是測試的一部分。 在更多的技術術語中,我將測試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");

好吧,這里我不僅測試了attributeWithDefault()符合規范,而且還測試了實現,因為我要求它使用Element.getAttribute() ,而不是Element.getAttributeNode().getValue()Element.getAttributes().getNamedItem().getNodeValue()等。

我認為我會以錯誤的方式解決它,所以任何有關如何改進我對模擬和最佳實踐的使用的提示都將受到贊賞。

編輯: 測試有什么問題

我做了上面的假設,測試是一種糟糕的風格,這是我的理由。

  1. 規范沒有指定調用哪個方法。 例如,只要正確完成,庫的客戶端就不應該關心如何檢索屬性。 實施者應該有自由的權利,以他認為合適的任何方式訪問任何替代方法(關於性能,一致性等)。 這是Element的規范,確保所有這些方法返回相同的值。

  2. 使用getElement() Element重新分解為單個方法接口是沒有意義的(Go實際上非常好)。 為了便於使用,該方法的客戶端應該只能在標准庫中使用標准Element 擁有接口和新類只是愚蠢,恕我直言,因為它使客戶端代碼難看,而且它不值得。

  3. 假設規范保持不變並且測試保持不變,新開發人員可能會決定重構代碼以使用不同的使用狀態的方法,並導致測試失敗! 那么,當實際實現符合規范時,測試失敗是有效的。

  4. 讓協作者以多種格式公開狀態是很常見的。 規范和測試不應取決於采用哪種特定方法; 只有實施應該!

這是模擬測試中的常見問題,一般的咒語就是:

只有您擁有的模擬類型

在這里,如果你想模擬與XML解析器的協作(不一定需要,老實說,因為一個小的測試XML應該在單元上下文中工作得很好),那么XML解析器應該在你擁有的將要處理的接口或類后面您需要調用第三方API上哪種方法的混亂細節。 重點是它有一個從元素中獲取屬性的方法。 嘲笑那個方法。 這將實現與設計分開。 真正的實現將有一個真正的單元測試,實際上測試你從真實對象獲得一個成功的元素。

模擬可以是一種保存樣板設置代碼的好方法(基本上用作Stubs),但這不是它們在驅動設計方面的核心目的。 模擬測試行為(與狀態相反)並且不是Stubs

我應該補充一點,當你使用Mocks作為存根時,它們看起來就像你的代碼。 任何存根都必須假設您將如何調用它與您的實現相關聯。 這很正常。 如果這是一個問題,那就是以不良方式推動您的設計。

在設計單元測試時,您將始終有效地測試您的實現,而不是一些抽象的規范。 或者可以說你將測試“技術規范”,這是通過技術細節擴展的業務規范。 這沒什么不對。 而不是測試:

我的方法將返回一個值,如果已定義或默認值。

你正在測試:

我的方法將返回一個值(如果已定義)或默認值,前提是當我調用getAttribute(name)時,提供的xml元素將返回此屬性。

我在這里可以看到的唯一解決方案(我必須承認我不熟悉你正在使用的庫)是創建一個包含所有功能的模擬元素,也就是說,也有能力設置getAttributeNote()。getValue()和getAttributes()。getNamedItem()。getNodeValue()的值。

但是,假設它們都是等價的,那么測試一個就好了。 當它變化時,您需要測試所有情況。

我發現你使用嘲笑沒有任何問題。 您正在測試的是attributeWithDefault()方法及其實現,而不是Element是否正確。 所以你嘲笑Element以減少所需的設置量。 該測試確保attributeWithDefault()的實現符合規范,自然需要一些可以為測試運行的特定實現。

你在這里有效地測試你的模擬對象。 如果要測試attributeWithDefault()方法,則必須斷言e.getAttribute()使用期望參數調用並忘記返回值。 此返回值僅驗證模擬對象的設置。 (我不知道這是用Java的mockito完成的,我是一個純粹的C#家伙...)

這取決於通過調用getAttribute()獲取屬性是否是規范的一部分,或者它是否是可能更改的實現細節。

如果Element是一個接口,那么聲明你應該使用'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;
     }
   }
}

然后,您將針對Mock AttributeProvider而不是Element測試attributeWithDefault。

當然在這種情況下它可能是一種矯枉過正,即使有一個實現你的測試可能也沒問題(你必須在某處測試它;))。 然而,如果邏輯在getAttribute或attributeWithDefualt中變得更復雜,那么這種解耦可能是有用的。

希望這會有所幫助。

在我看來,您希望使用此方法驗證3件事:

  1. 它從正確的位置獲取屬性(Element.getAttribute())
  2. 如果該屬性不為null,則返回該屬性
  3. 如果該屬性為null,則返回字符串“default”

您目前正在驗證#2和#3,但不是#1。 使用mockito,您可以通過添加驗證#1

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

這確保了實際在模擬上調用方法。 不可否認,這在mockito中有點笨拙。 在easymock中,您可以執行以下操作:

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

它具有相同的效果,但我認為讓您的測試更容易閱讀。

如果您使用依賴注入,那么協作者應該是合同的一部分。 您需要能夠通過構造函數或公共屬性注入所有協作者。

一句話:如果你有一個合作者,你新近而不是注入,那么你可能需要重構代碼。 這是測試/模擬/注入所需的思維方式的變化。

這是一個遲到的答案,但它與其他觀點有不同的觀點。

基本上,由於他在問題中所說的原因,OP正確認為嘲弄測試是不好的。 那些說嘲諷沒問題的人沒有提供充分的理由,IMO。

這是一個完整版本的測試,有兩個版本:一個是模擬(BAD一個),另一個沒有(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;
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM