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, даже если это может привести к правильному результату

Итак, могу ли я протестировать этот код изолированно? Похоже, я не могу.. есть ли название для этого типа зависимости, которое нельзя высмеивать, поскольку его поведение по сути требуется для тестируемого метода?

Author: greut, 2015-08-10

3 answers

Это вопрос, касающийся TDD. TDD означает, что сначала нужно написать тест.

Я не могу представить, как начать с теста, который издевается над DOMElement::createElement, прежде чем писать фактическую реализацию. Естественно, что вы начинаете с объекта и ожидаемого xml.

Кроме того, я бы не назвал DOMElement зависимостью. Это личная деталь вашей реализации. Вы никогда не передадите другую реализацию DOMElement конструктору XMLSerializer, поэтому нет необходимости раскрывать ее в конструкторе.

Тесты также должен служить документацией. Простой тест с объектом и ожидаемым xml будет читабельным. Каждый сможет прочитать его и быть уверенным в том, чем занимается ваш класс. Сравните это с 50-строчным тестом с насмешкой (насмешки PHPUnit нелепо многословны).

РЕДАКТИРОВАТЬ: Вот хорошая статья об этом http://www.jmock.org/oopsla2004.pdf . В двух словах говорится, что, если вы не используете тесты для управления своим дизайном (поиска интерфейсов), нет смысла использовать насмешки.

Существует также хорошее правило

Только Макетные Типы, Которыми Вы Владеете

(упоминается в статье), который может быть применен к вашему примеру.

 11
Author: woru, 2015-08-13 18:35:45

Как вы уже упоминали, изоляция тестов - хороший метод, если вы хотите ускорить устранение ошибок. Однако написание этих тестов может потребовать значительных затрат как с точки зрения разработки, так и с точки зрения обслуживания. В конце концов, то, что вам действительно нужно, - это набор тестов, который не нужно менять каждый раз, когда вы изменяете тестируемую систему. Другими словами, вы пишете тест против API, а не против его деталей реализации.

Конечно, однажды ты может возникнуть труднодоступная ошибка, для обнаружения которой потребуется тестовая изоляция, но она может вам не понадобиться прямо сейчас. Поэтому я бы предложил сначала протестировать входы и выходы вашей системы (сквозной тест). Если однажды вам понадобится больше, что ж, вы все равно сможете провести еще несколько детализированных тестов.

Возвращаясь к вашей проблеме, то, что вы действительно хотите проверить, - это логика преобразования, которая выполняется в сериализаторе, независимо от того, как это делается. Издеваешься над типом, которого ты не знаешь собственный вариант не подходит, так как произвольные предположения о том, как класс взаимодействует со своей средой, могут привести к проблемам после развертывания кода. Как было предложено m1lt0n, вы можете инкапсулировать этот класс в интерфейс и имитировать его для целей тестирования. Это дает некоторую гибкость в отношении реализации сериализатора, но реальный вопрос в том, действительно ли вам это нужно? Каковы преимущества по сравнению с более простым решением? Для первой реализации, похоже, я считаю, что простого теста ввода и вывода должно быть достаточно ("Держите его простым и глупым"). Если когда-нибудь вам понадобится переключаться между различными стратегиями сериализатора, просто измените дизайн и добавьте некоторую гибкость.

 1
Author: Francis Toth, 2015-08-16 19:59:19

Позвольте мне ответить на ваши вопросы/проблемы, которые вы видите в коде и тестах:

1) На самом деле я вообще не тестирую метод, все, что я проверяю, это то, что метод возвращает результат $document->saveXML()

Правильно, издеваясь над DOMDocument, и его методы возвращаются таким образом, вы просто проверяете, что метод будет вызван (даже не то, что метод возвращает результат saveXML(), так как я не вижу утверждения для метода сериализации, а просто вызывая его, что вызывает ожидание, чтобы быть правдой).

2) Тесту известно о реализации метода (он использует domdocument для генерации xml)

Это также верно и очень важно, потому что, если внутренняя реализация метода изменится, тест может завершиться неудачей, даже если он вернет правильный результат. Тест должен рассматривать метод как "черный ящик", беспокоясь только о возвращаемом значении метода с набором заданных аргументов.

3) Тест завершится неудачей, если класс будет переписан для использования simplexml или другой библиотеки xml, даже если это может привести к правильному результату

Верно, см. Мой комментарий к (2)

Итак, какова же тогда альтернатива? Учитывая вашу реализацию XmlSerializer, DOMDocument просто облегчает/является помощником для фактического выполнения сериализации. Кроме этого, метод просто повторяет свойства объекта. Таким образом, XmlSerializer и DOMDocument являются в каком-то смысле неразлучны, и это может быть просто прекрасно.

Что касается самого теста, мой подход состоял бы в том, чтобы предоставить известный объект и утверждать, что метод сериализации возвращает ожидаемую структуру 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, но это сделает код, использующий сериализатор, тестируемым, так что это отчасти актуально.

 0
Author: m1lt0n, 2015-08-14 09:48:57