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);
У которого есть несколько проблем:
- На самом деле я вообще не тестирую этот метод, все, что я проверка заключается в том, что метод возвращает результат
$document->saveXML()
- Тесту известно о реализации метода (он использует domdocument для генерации xml)
- Тест завершится неудачей, если класс будет переписан для использования simplexml или другой библиотеки xml, даже если это может привести к правильному результату
Итак, могу ли я протестировать этот код изолированно? Похоже, я не могу.. есть ли название для этого типа зависимости, которое нельзя высмеивать, поскольку его поведение по сути требуется для тестируемого метода?
3 answers
Это вопрос, касающийся TDD. TDD означает, что сначала нужно написать тест.
Я не могу представить, как начать с теста, который издевается над DOMElement::createElement
, прежде чем писать фактическую реализацию. Естественно, что вы начинаете с объекта и ожидаемого xml.
Кроме того, я бы не назвал DOMElement
зависимостью. Это личная деталь вашей реализации. Вы никогда не передадите другую реализацию DOMElement
конструктору XMLSerializer
, поэтому нет необходимости раскрывать ее в конструкторе.
Тесты также должен служить документацией. Простой тест с объектом и ожидаемым xml будет читабельным. Каждый сможет прочитать его и быть уверенным в том, чем занимается ваш класс. Сравните это с 50-строчным тестом с насмешкой (насмешки PHPUnit нелепо многословны).
РЕДАКТИРОВАТЬ: Вот хорошая статья об этом http://www.jmock.org/oopsla2004.pdf . В двух словах говорится, что, если вы не используете тесты для управления своим дизайном (поиска интерфейсов), нет смысла использовать насмешки.
Существует также хорошее правило
Только Макетные Типы, Которыми Вы Владеете
(упоминается в статье), который может быть применен к вашему примеру.
Как вы уже упоминали, изоляция тестов - хороший метод, если вы хотите ускорить устранение ошибок. Однако написание этих тестов может потребовать значительных затрат как с точки зрения разработки, так и с точки зрения обслуживания. В конце концов, то, что вам действительно нужно, - это набор тестов, который не нужно менять каждый раз, когда вы изменяете тестируемую систему. Другими словами, вы пишете тест против API, а не против его деталей реализации.
Конечно, однажды ты может возникнуть труднодоступная ошибка, для обнаружения которой потребуется тестовая изоляция, но она может вам не понадобиться прямо сейчас. Поэтому я бы предложил сначала протестировать входы и выходы вашей системы (сквозной тест). Если однажды вам понадобится больше, что ж, вы все равно сможете провести еще несколько детализированных тестов.
Возвращаясь к вашей проблеме, то, что вы действительно хотите проверить, - это логика преобразования, которая выполняется в сериализаторе, независимо от того, как это делается. Издеваешься над типом, которого ты не знаешь собственный вариант не подходит, так как произвольные предположения о том, как класс взаимодействует со своей средой, могут привести к проблемам после развертывания кода. Как было предложено m1lt0n, вы можете инкапсулировать этот класс в интерфейс и имитировать его для целей тестирования. Это дает некоторую гибкость в отношении реализации сериализатора, но реальный вопрос в том, действительно ли вам это нужно? Каковы преимущества по сравнению с более простым решением? Для первой реализации, похоже, я считаю, что простого теста ввода и вывода должно быть достаточно ("Держите его простым и глупым"). Если когда-нибудь вам понадобится переключаться между различными стратегиями сериализатора, просто измените дизайн и добавьте некоторую гибкость.
Позвольте мне ответить на ваши вопросы/проблемы, которые вы видите в коде и тестах:
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, но это сделает код, использующий сериализатор, тестируемым, так что это отчасти актуально.