2009-11-19 37 views
7

我刚开始在我的测试中使用模拟对象(使用Java的mockito)。不用说,他们简化了测试的设置部分,并且与依赖注入一起,我认为它使代码更加健壮。Mocks在测试中的使用

但是,我发现自己在测试实施而不是规格测试中绊倒。我最终设定了期望,我会认为它不是测试的一部分。用更专业的术语来说,我将测试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. Element重新分解为单个方法接口getElement()(实际上Go非常好),没有任何意义。为了便于使用,该方法的客户应该能够仅使用标准库中的标准Element。拥有接口和新类只是简单的傻瓜,恕我直言,因为它使客户端代码丑陋,这是不值得的。

  3. 假设规范保持原样并且测试保持不变,新开发人员可能决定重构代码以使用不同的方法来使用状态,并导致测试失败!那么,当实际实现符合规范时,测试失败是有效的。

  4. 使协作者以多种格式公开状态是很常见的。规范和测试不应取决于采取哪种特定方法;只有实施应该!

回答

6

这是在模拟测试的一个常见问题,和一般的口头禅脱身从这个是:

Only mock types you own

在这里,如果你想嘲笑(不一定需要,说实话,作为一个小测试XML应该工作在单位范围内就好了)与XML解析器合作那么XML解析器应该是一个接口或类背后您拥有的将处理您需要调用的第三方API的哪种方法的混乱细节。重点是它有一个从元素获取属性的方法。嘲笑那个方法。这将实施与设计分开。真正的实现将有一个真正的单元测试,实际上测试你从一个真实的对象获得成功的元素。

Mocks可以是一种节省样板设置代码(基本上作为存根)的好方法,但这不是它们在驱动设计方面的核心目的。嘲笑正在测试行为(而不是状态),并且是not Stubs

我应该补充说,当你使用Mocks作为存根时,它们看起来就像你的代码。任何存根必须假设你将如何称之为与你的实现相关联。这很正常。问题出在哪里,如果这是以糟糕的方式推动你的设计。

+1

+1。就像你如何澄清行为(模拟)与状态(存根)测试之间的区别。 – notnoop 2009-11-19 18:18:22

0

唯一的解决办法,我能为你在这里看到(我不得不承认,我不熟悉您使用的库)是创建了所有的功能包括一个模拟元素,也可以设置getAttributeNote()。getValue()和getAttributes()。getNamedItem()。getNodeValue()的值。

但是,假设它们都是等效的,那么只测试一个就可以了。这是因为你需要测试所有的情况。

1

在设计单元测试时,您将始终有效地测试您的实现,而不是某些抽象规范。或者可以争辩说,你会测试“技术规范”,这是业务规范扩展的技术细节。这没有什么错。而不是测试:

我的方法将返回一个值,如果定义或默认。

正在测试:如果定义

我的方法将返回一个值或默认提供的提供,当我打电话的getAttribute(名称)将返回这个属性的XML元素。

+0

你是正确的搞清楚什么是测试。但是,我认为方法的公共契约不应该那么严格。 – notnoop 2009-11-19 18:09:25

0

我没有发现你使用模拟器有什么问题。你正在测试的是attributeWithDefault()方法,它是实现,而不是Element是否正确。所以你嘲笑Element为了减少所需设置的数量。测试确保attributeWithDefault()的实现符合规范,当然需要一些可以为测试运行的特定实现。

0

你在这里有效地测试你的模拟对象。 如果要测试attributeWithDefault()方法,则必须声明e.getAttribute()被调用期望的参数,并忘记返回值。此返回值仅验证您的模拟对象的设置。 (我不知道这是如何完全用Java的mockito完成的,我是一个纯粹的C#人...)

+1

在我看来,测试验证了被测试的类根据'e.getAttribute()'的返回值改变了它的行为。它不测试模拟模块返回正确的值,而是根据返回值改变类的行为。 – Yishai 2009-11-19 16:45:17

0

它取决于通过调用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中,这种解耦可能是有用的。

希望这有助于。

+0

我同意你这是一个矫枉过正的问题。我更新了这篇文章。 – notnoop 2009-11-19 18:10:10

+0

您提到了“getElement的单个接口”;你的意思是“与getAttribute单个接口”? 更新后:如果我理解正确,您想屏蔽attributeWithDefault实际上知道如何从元素中获取属性。我知道,另一种强制执行的方法是添加另一个间接级别(而不是传递元素,而是传递另一个知道如何从元素获取属性的对象)。不过,我认为,无论如何,在某个时刻,你需要一个知道如何从一个元素获取属性的类。 – phtrivier 2009-11-20 08:09:20

0

在我看来,有要验证这个方法三两件事:

  1. 它从正确的地方属性(Element.getAttribute())
  2. 如果属性不空,则返回
  3. 如果属性为null,字符串 “默认” 返回

您目前正在核实#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); 

它具有相同的效果,但我认为使您的测试更容易阅读。

+0

@chrispix。有三个“正确的地方”来获取属性。我写的测试只验证其中一个被使用。实现可以自由选择任何一个。测试不应该关心它被检索的地方。 – notnoop 2009-11-19 18:11:36

+0

如果您希望测试对您的类与它的依赖关系如何协作是不可知的,那么您不应该嘲笑依赖关系 - 而应该像使用Yishai所说的那样使用测试XML片段。 根据你的类的复杂程度,这可能不现实 - 管理测试XML(或其他结构化)的数据可能是一个巨大的痛苦,并使你的测试变得脆弱。 – 2009-11-19 19:51:27

+0

我也不认为这是一个很大的问题,如果测试时有人重构实施。他们重构使用不同的方法可能是有效的,但更新测试也很容易。 在这种情况下,失败的测试将作为警告,以确保重构确实有效。我已经让开发人员以一种他们认为遵守规范的方式重构了一个实现,但是却是错误的。一个更好的测试会暴露这个错误。 – 2009-11-19 19:55:06

0

如果您使用依赖注入,那么协作者应该成为合同的一部分。您需要能够通过构造函数或公共属性注入所有协作者。底线:如果你有一个合作者,而不是注入,那么你可能需要重构代码。这是测试/嘲笑/注入所必需的心态变化。

+0

另外,当我看着你的具体代码示例时,你没有指定'Element e'如何传递给你的方法。它是注入到对象还是作为参数传递?在这种情况下,使用具有数据设置的具体对象来匹配测试用例而不是使用模拟可能更合理。 – Brett 2009-11-19 18:13:34

+0

你的意思是说当采用'DI'时,我需要明确指定如何调用和调用哪些方法。当规范说明'列表不是'时,我是否需要澄清我将调用'!list.isEmpty()'而不是'list.size()!= 0'。这不会污染合同吗? – notnoop 2009-11-19 18:14:17

+0

@Brett,我假设这是一种静态方法。 – notnoop 2009-11-19 18:14:55

0

这是一个迟到的答案,但它需要与其他方面不同的观点。

基本上,由于他在问题中陈述的原因,OP在考虑嘲笑测试时是正确的。那些说嘲笑确定的人没有提供充足的理由,国际海事组织。

这里是一个完整版本的测试,在两个版本:一个嘲笑(坏的一个)和另一个没有(好的一个)。 (我冒昧地使用了不同的嘲讽图书馆,但这并没有改变这一点。)

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; 
    } 
} 
相关问题