Подтвердить что ты не робот

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

}

Что, кажется, решает проблему, однако, теперь мой тест на самом деле ничего не делает. Мне нужно сделать mock 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, даже если он может привести к правильному результату.

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

4b9b3361

Ответ 1

Это вопрос о TDD. TDD означает сначала запись теста.

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

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

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

EDIT: Вот хорошая статья об этом http://www.jmock.org/oopsla2004.pdf. В двух словах говорится, что, если вы не используете тесты для управления вашим дизайном (найдите интерфейсы), они мало ориентируются на использование mocks.

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

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