【问题标题】:TDD - Dependencies that cannot be mockedTDD - 无法模拟的依赖项
【发布时间】:2015-08-10 09:14:36
【问题描述】:

假设我有一堂课:

class XMLSerializer {
    public function serialize($object) {
        $document = new DomDocument();
        $root = $document->createElement('object');
        $document->appendChild($root);

        foreach ($object as $key => $value) {
            $root->appendChild($document->createElement($key, $value);
        }

        return $document->saveXML();
    }

    public function unserialze($xml) {
        $document = new DomDocument();
        $document->loadXML($xml);

        $root = $document->getElementsByTagName('root')->item(0);

        $object = new stdclass;
        for ($i = 0; $i < $root->childNodes->length; $i++) {
            $element = $root->childNodes->item($i);
            $tagName = $element->tagName;
            $object->$tagName = $element->nodeValue();
        }

        return $object;
    }

}

如何单独测试?在测试这个类的时候,我也在测试DomDocument类

我可以传入文档对象:

class XMLSerializer {
    private $document;

    public function __construct(\DomDocument $document) {
        $this->document = $document;
    }

    public function serialize($object) {
        $root = $this->document->createElement('object');
        $this->document->appendChild($root);

        foreach ($object as $key => $value) {
            $root->appendChild($this->document->createElement($key, $value);
        }

        return $this->document->saveXML();
    }

    public function unserialze($xml) {
        $this->document->loadXML($xml);

        $root = $this->document->getElementsByTagName('root')->item(0);

        $object = new stdclass;
        for ($i = 0; $i < $root->childNodes->length; $i++) {
            $element = $root->childNodes->item($i);
            $tagName = $element->tagName;
            $object->$tagName = $element->nodeValue();
        }

        return $object;
    }

}

这似乎解决了问题,但是,现在我的测试并没有真正做任何事情。我需要让一个模拟 DomDocument 返回我在测试中测试的 XML:

$object = new stdclass;
$object->foo = 'bar';

$mockDocument = $this->getMock('document')
                ->expects($this->once())
                ->method('saveXML')
                ->will(returnValue('<?xml verison="1.0"?><root><foo>bar</foo></root>'));

$serializer = new XMLSerializer($mockDocument);

$serializer->serialize($object);

这有几个问题:

  1. 我实际上并没有测试该方法,我只是检查该方法是否返回$document-&gt;saveXML()的结果
  2. 测试知道方法的实现(它使用domdocument生成xml)
  3. 如果将类重写为使用 simplexml 或其他 xml 库,则测试将失败,即使它可能会产生正确的结果

所以我可以单独测试这段代码吗?看起来我不能......这种依赖类型是否有一个不能被模拟的名称,因为它的行为本质上是被测试方法所必需的?

【问题讨论】:

  • 为什么需要单独测试?
  • 因为在实际用例中,如果测试失败,依赖项不是内置的(或 DomDocument),我不知道问题是否与我正在测试的方法的实现有关,或它构造的对象之一。当然,我可以对它们进行单独的测试,但是在开发过程中每次都运行所有测试是低效的。正如这里所说:msdn.microsoft.com/en-us/library/hh549175.aspx“通过隔离代码进行测试,您知道如果测试失败,原因就在那里,而不是其他地方”,如果测试被隔离,它只会使开发/调试更快。

标签: php unit-testing testing phpunit tdd


【解决方案1】:

这是一个关于 TDD 的问题。 TDD 意味着先写测试。

我无法想象在编写实际实现之前从一个模拟 DOMElement::createElement 的测试开始。从对象和预期的 xml 开始是很自然的。

另外,我不会将 DOMElement 称为依赖项。这是您的实施的私人细节。您永远不会将 DOMElement 的不同实现传递给 XMLSerializer 的构造函数,因此无需在构造函数中公开它。

测试也应该作为文档。带有对象和预期 xml 的简单测试将是可读的。每个人都可以阅读它并确定您的班级在做什么。将此与带有模拟的 50 行测试进行比较(PhpUnit 模拟非常冗长)。

编辑: 这是一篇关于它的好论文http://www.jmock.org/oopsla2004.pdf。 简而言之,它指出,除非您使用测试来驱动您的设计(查找接口),否则使用模拟几乎没有意义。

还有一个很好的规则

只有你拥有的模拟类型

(在论文中提到)可以应用于您的示例。

【讨论】:

  • 我认为这是最明智的答案。不应嘲笑私有实施细节。当然,这确实存在不隔离测试的问题,但在这种情况下问题很小。
【解决方案2】:

正如您所提到的,如果您想加快错误解决速度,测试隔离是一种很好的技术。但是,编写这些测试在开发和维护方面可能会产生重要的成本。 归根结底,您真正想要的是一个不必在每次修改被测系统时都更改的测试套件。 换句话说,您针对 API 编写测试,不反对其实施细节。

当然,有一天您可能会遇到一个需要测试隔离才能被发现的难以发现的错误,但您现在可能不需要它。因此,我建议首先测试系统的输入和输出(端到端测试)。如果有一天,您需要更多,那么您仍然可以进行一些更细粒度的测试。

回到你的问题,你真正想要测试的是在序列化器中完成的转换逻辑,不管它是如何完成的。模拟您不拥有的类型不是一种选择,因为对类如何与其环境进行交互做出任意假设可能会在部署代码后导致您遇到问题。正如 m1lt0n 所建议的,您可以将此类封装在一个接口中,并模拟它以进行测试。这为序列化程序的实现提供了一些灵活性,但真正的问题是,你真的需要吗? 与更简单的解决方案相比有什么好处?对于第一个实现,似乎对我来说,一个简单的输入与输出测试就足够了(“保持简单和愚蠢”)。如果有一天您需要在不同的序列化器策略之间切换,只需更改设计并增加一些灵活性。

【讨论】:

    【解决方案3】:

    让我解决您在代码和测试中看到的问题/问题:

    1) 我实际上并没有测试该方法,我只是检查该方法是否返回 $document->saveXML() 的结果

    没错,通过模拟 DomDocument 并且它的方法以这种方式返回,您只需检查该方法是否会被调用(甚至该方法是否返回 saveXML() 的结果,因为我没有看到断言serialize 方法,但只是调用它,这会触发期望为真)。

    2)测试知道方法的实现(它使用domdocument生成xml)

    这也是正确且非常重要的,因为如果方法的内部实现发生变化,即使返回正确的结果,测试也可能会失败。测试应该将方法视为“黑盒”,只关心具有一组给定参数的方法的返回值。

    3) 如果将类重写为使用 simplexml 或其他 xml 库,则测试将失败,即使它可能会产生正确的结果

    没错,请参阅我对 (2) 的评论

    那么,还有什么选择呢?鉴于您对 XMLSerializer 的实现,DomDocument 只是促进/是实际执行序列化的助手。除此之外,该方法只是迭代对象的属性。所以 XMLSerializer 和 DomDocument 在某种程度上是不可分割的,这可能很好。

    关于测试本身,我的方法是提供一个已知对象并断言 serialize 方法返回预期的 xml 结构(由于对象已知,结果也已知)。这样,您就不会被方法的实际实现所束缚(因此无论您使用 DomDocument 还是其他东西来实际执行 XML 文档创建)。

    现在,关于您提到的另一件事(注入 DomDocument),它在当前实现中没有用。为什么?因为如果您想使用其他工具来创建 XML 文档(您提到的 simplexml 等),您需要更改方法的主要部分。另一种实现如下:

    <?php
    
        interface Serializer
        {
          public function serialize($object);
    
          public function unserialize($xml);
        }
    
    
        class DomDocumentSerializer
        {
          public function serialize($object)
          {
         // the actual implementation, same as the sample code you provide
          }
    
          public function unserialize($xml)
          {
         // the actual implementation, same as the sample code you provide
          }
        }
    

    上述实现的好处是,当您需要序列化器时,您可以键入提示接口并注入任何实现,因此下次创建新的 SimplexmlSerializer 实现时,您只需要通过类的实例化需要(这就是依赖注入有意义的地方)一个序列化程序作为参数,并且只需更改实现。

    对不起最后一部分和代码,它可能有点偏离 TDD 的目的,但它会使使用序列化器的代码可测试,所以它有点相关。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-12-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-28
      相关资源
      最近更新 更多