Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TDD - Dependencies that cannot be mocked

Let's say I have a class:

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;
    }

}

How do I test this in isolation? When testing this class, I am also testing the DomDocument class

I could pass in the document object:

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;
    }

}

Which seems to solve the problem, however, now my test isn't really doing anything. I need to make a mock DomDocument return the XML I'm testing in the test:

$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);

Which has several problems:

  1. I'm not actually testing the method at all, all I'm checking is that the method returns the result of $document->saveXML()
  2. The test is aware of the implementation of the method (it uses domdocument to generate the xml)
  3. The test will fail if the class is rewritten to use simplexml or another xml library, even though it could be producing the correct result

so can I test this code in isolation? It looks like I can't.. is there a name for this type of dependency that can't be mocked as its behaviour is essentially required for the method being tested?

like image 640
Tom B Avatar asked Aug 10 '15 09:08

Tom B


2 Answers

This is a question regarding TDD. TDD means writing test first.

I cannot imagine starting with a test that mocks DOMElement::createElement before writing the actual implementation. It's natural that you start with an object and expected xml.

Also, I wouldn't call DOMElement a dependency. It's a private detail of your implementation. You will never pass a different implementation of DOMElement to the constructor of XMLSerializer so there's no need to expose it in the constructor.

Tests should also serve as a documentation. Simple test with an object and expected xml will be readable. Everyone will be able to read it and be sure what your class is doing. Compare this to 50 line test with mocking (PhpUnit mocks are preposterously verbose).

EDIT: Here's a good paper about it http://www.jmock.org/oopsla2004.pdf. In a nutshell it states that unless you use tests to drive your design (find interfaces), there’s little point to using mocks.

There is also a good rule

Only Mock Types You Own

(mentioned in the paper) that can be applied to your example.

like image 132
woru Avatar answered Oct 15 '22 12:10

woru


As you've mentioned it, test isolation is a good technique if you want to speed-up bug resolution. However, writing those test can have an important cost in terms of development as well as in terms of maintenance. At the end of the day, what you really is want is a test suite that does not have to change each time you modify the system under test. In other words, you write a test against an API, not against its details of implementation.

Of course, one day you may encounter a hard-to-find bug that would require test isolation in order to be spotted, but you may not need it right now. Therefore, I would suggest to test the inputs and the outputs of your system first (end-to-end test). If one day, you need more, well, you'll still be able to make some more fined grained tests.

Back to your problem, what you really want to test, is the transformation logic which is done in the serializer, no matter how it is done. Mocking a type you don't own is not an option, as making arbitrary assumptions regarding how a class interacts with its environment can lead you to problems once the code is deployed. As suggested by m1lt0n, you can encapsulate this class within a interface, and mock it for test purpose. This gives some flexibility regarding the serializer's implementation but the real question is, do you really need that ? What are the benefits compared to simpler solution ? For a first implementation, it seems to me that a simple input vs output test should be enough ("Keep it simple and stupid"). If some day you need to switch between different serializer strategy, just change the design and add some flexibility.

like image 38
Francis Toth Avatar answered Oct 15 '22 13:10

Francis Toth