TDD - 无法模拟的依赖关系

时间:2015-08-10 09:14:36

标签: php unit-testing testing phpunit tdd

假设我有一个班级:

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->saveXML()的结果
  2. 测试知道该方法的实现(它使用domdocument生成xml)
  3. 如果将类重写为使用simplexml或其他xml库,则测试将失败,即使它可能产生正确的结果
  4. 我可以单独测试这段代码吗?它看起来我不能..这种类型的依赖项是否有一个名称无法模拟,因为它的行为基本上是被测试方法所必需的?

3 个答案:

答案 0 :(得分:11)

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

我无法想象在编写实际实现之前,先从一个模拟DOMElement::createElement的测试开始。你很自然地从一个对象开始并期望xml。

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

测试也应作为文档。使用对象和预期的xml进行简单测试是可读的。每个人都可以阅读它,并确保你的班级正在做什么。将此与50行测试与模拟相比较(PhpUnit模拟是荒谬的冗长)。

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

还有一个很好的规则

  

只有你拥有的模拟类型

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

答案 1 :(得分:1)

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

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

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

答案 2 :(得分:0)

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

  

1)我根本没有测试该方法,我正在检查的是该方法返回$ document-&gt; 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的目的,但它会使使用序列化程序的代码可测试,所以它是相关的。