在测试中使用模拟

时间:2009-11-19 16:01:18

标签: c# java unit-testing testing mocking

我刚开始在我的测试中使用模拟对象(使用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()或{ {1}}等等。

我认为我会以错误的方式解决这个问题,所以我们将非常感谢有关如何改进我对模拟和最佳实践的使用的任何提示。

编辑: 测试有什么问题

我做了上面的假设,测试是一种不好的风格,这是我的理由。

  1. 规范未指定调用哪个方法。例如,只要正确完成,库的客户端就不应该关心如何检索属性。实施者应该有自由的权利,以他认为合适的任何方式访问任何替代方法(关于性能,一致性等)。 Element.getAttributes().getNamedItem().getNodeValue()的规范确保所有这些方法都返回相同的值。

  2. Element重新分解为具有Element的单个方法接口是没有意义的(实际上Go非常好)。为了便于使用,该方法的客户端应该只能在标准库中使用标准getElement()。拥有接口和新类只是愚蠢,恕我直言,因为它使客户端代码变得丑陋,而且它不值得。

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

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

9 个答案:

答案 0 :(得分:6)

这是模拟测试中的一个常见问题,一般的咒语就是:

  

Only mock types you own

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

模拟可以很好地保存样板设置代码(基本上用作Stubs),但这不是它们在驱动设计方面的核心目的。模拟测试行为(与状态相反)并且是not Stubs

我应该补充说,当你使用Mocks作为存根时,它们看起来就像你的代码。任何存根都必须假设您将如何调用它与您的实现相关联。这很正常。问题在于,如果这会以不好的方式推动您的设计。

答案 1 :(得分:1)

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

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

你正在测试:

我的方法将返回一个值(如果已定义)或默认值,前提是当我调用getAttribute(name)时,提供的xml元素将返回此属性。

答案 2 :(得分:0)

我能在这里看到的唯一解决方案(我不得不承认我不熟悉您正在使用的库)是创建一个包含所有功能的模拟元素,也就是说,能够设置getAttributeNote()。getValue()和getAttributes()。getNamedItem()。getNodeValue()。

的值。

但是,假设它们都是等价的,那么只测试一个就好了。当它变化时,您需要测试所有情况。

答案 3 :(得分:0)

我发现你使用嘲笑没有任何问题。您正在测试的是attributeWithDefault()方法及其实现,而不是Element是否正确。所以你嘲笑Element以减少所需的设置量。该测试确保attributeWithDefault()的实现符合规范,自然需要一些可以为测试运行的特定实现。

答案 4 :(得分:0)

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

答案 5 :(得分: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中变得更复杂,那么这种解耦可能是有用的。

希望这有帮助。

答案 6 :(得分:0)

在我看来,您希望使用此方法验证3件事:

  1. 它从正确的位置获取属性(Element.getAttribute())
  2. 如果该属性不为null,则返回
  3. 如果属性为null,则返回字符串“default”
  4. 您目前正在验证#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);
    

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

答案 7 :(得分:0)

如果您使用依赖注入,那么协作者应该是合同的一部分。您需要能够通过构造函数或公共属性注入所有协作者。

结论:如果你有一个合作者,你新近而不是注入,那么你可能需要重构代码。这是测试/模拟/注入所需的思维方式的改变。

答案 8 :(得分:0)

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

基本上,由于他在问题中说明的原因,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;
    }
}