Лучшие практики для тестирования защищенных методов с помощью PHPUnit

287

Я нашел дискуссию о том , тестируешь ли ты частный метод информативно.

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

Я думал о следующем:

  • Метод Object, как указано в ответе, кажется излишним для этого.
  • Начните с общедоступных методов и, когда покрытие кода обеспечивается тестами более высокого уровня, включите их защиту и удалите тесты.
  • Наследуйте класс с тестируемым интерфейсом, делая защищенные методы общедоступными

Что является лучшей практикой? Есть ли еще что-нибудь?

Кажется, что JUnit автоматически меняет защищенные методы на общедоступные, но я не смотрел на это глубже. PHP не позволяет этого через отражение .

GrGr
источник
Два вопроса: 1. Почему вы должны беспокоиться о тестировании функциональности, которую ваш класс не раскрывает? 2. Если вы должны проверить это, почему это личное?
nad2000
2
Возможно, он хочет проверить, правильно ли задано частное свойство, и единственный способ проверки с использованием только функции установщика - сделать частную собственность общедоступной и проверить данные
AntonioCS
4
И поэтому это стиль обсуждения и, следовательно, не конструктивный. Опять :)
mlvljr
72
Вы можете назвать это вопреки правилам сайта, но просто назвать его «неконструктивным» ... это оскорбительно.
Энди V
1
@ Visser, это оскорбляет себя;)
Pacerier

Ответы:

417

Если вы используете PHP5 (> = 5.3.2) с PHPUnit, вы можете протестировать свои закрытые и защищенные методы, используя отражение, чтобы сделать их общедоступными до запуска ваших тестов:

protected static function getMethod($name) {
  $class = new ReflectionClass('MyClass');
  $method = $class->getMethod($name);
  $method->setAccessible(true);
  return $method;
}

public function testFoo() {
  $foo = self::getMethod('foo');
  $obj = new MyClass();
  $foo->invokeArgs($obj, array(...));
  ...
}
uckelman
источник
27
Цитировать ссылку на блог Себастьяна: «Итак: если тестирование защищенных и закрытых атрибутов и методов возможно, это еще не значит, что это« хорошая вещь »». - Просто чтобы иметь это в виду
Едиорян
10
Я бы оспорил это. Если вам не нужны защищенные или приватные методы для работы, не проверяйте их.
Uckelman
10
Просто чтобы уточнить, вам не нужно использовать PHPUnit для этого, чтобы работать. Это также будет работать с SimpleTest или чем-то еще. В ответе нет ничего, что зависит от PHPUnit.
Ян Данн
84
Вы не должны тестировать защищенных / частных пользователей напрямую. Они принадлежат к внутренней реализации класса и не должны сочетаться с тестом. Это делает рефакторинг невозможным, и в конечном итоге вы не проверяете то, что нужно проверить. Вы должны проверить их косвенно, используя публичные методы. Если вам это трудно, почти наверняка есть проблема с составом класса, и вам нужно разделить его на более мелкие классы. Имейте в виду, что ваш класс должен быть черным ящиком для вашего теста - вы добавляете что-то и получаете что-то обратно, и это все!
gphilip
24
@gphilip Для меня protectedметод также является частью общедоступного API, потому что любой сторонний класс может расширить его и использовать без всякой магии. Поэтому я думаю, что только privateметоды попадают в категорию методов, которые не должны быть непосредственно проверены. protectedи publicдолжен быть непосредственно проверен.
Филипп Халакса
48

Вы, кажется, уже знаете, но я все равно просто повторю это; Это плохой знак, если вам нужно проверить защищенные методы. Целью модульного тестирования является тестирование интерфейса класса, а защищенные методы являются деталями реализации. Тем не менее, есть случаи, когда это имеет смысл. Если вы используете наследование, вы можете видеть, что суперкласс обеспечивает интерфейс для подкласса. Итак, здесь вам нужно будет протестировать защищенный метод (но никогда не закрытый ). Решением этой проблемы является создание подкласса для целей тестирования и использование его для предоставления методов. Например.:

class Foo {
  protected function stuff() {
    // secret stuff, you want to test
  }
}

class SubFoo extends Foo {
  public function exposedStuff() {
    return $this->stuff();
  }
}

Обратите внимание, что вы всегда можете заменить наследование композицией. При тестировании кода, как правило, гораздо проще иметь дело с кодом, использующим этот шаблон, поэтому вы можете рассмотреть этот вариант.

troelskn
источник
2
Вы можете просто напрямую реализовать stuff () как public и вернуть parent :: stuff (). Смотрите мой ответ. Кажется, я читаю вещи слишком быстро сегодня.
Майкл Джонсон
Ты прав; Действительный способ изменить защищенный метод на публичный.
troelskn
Таким образом, код предлагает мой третий вариант и «Обратите внимание, что вы всегда можете заменить наследование композицией». идет в направлении моего первого варианта или refactoring.com/catalog/replaceInheritanceWithDelegation.html
GrGr
34
Я не согласен, что это плохой знак. Давайте сделаем разницу между TDD и модульным тестированием. Модульное тестирование должно тестировать частные методы imo, поскольку они являются модулями и выиграют точно так же, как модульное тестирование. Публичные методы выигрывают от модульного тестирования.
Коен
36
Защищенные методы являются частью интерфейса класса, а не просто деталями реализации. Весь смысл защищенных членов состоит в том, что субклассеры (пользователи сами по себе) могут использовать эти защищенные методы внутри исключений классов. Те явно должны быть проверены.
BT
40

Teastburn имеет правильный подход. Еще проще вызвать метод напрямую и вернуть ответ:

class PHPUnitUtil
{
  public static function callMethod($obj, $name, array $args) {
        $class = new \ReflectionClass($obj);
        $method = $class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($obj, $args);
    }
}

Вы можете назвать это просто в своих тестах:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );
robert.egginton
источник
1
Это отличный пример, спасибо. Метод должен быть публичным, а не защищенным, не так ли?
Вальк
Хорошая точка зрения. Я на самом деле использую этот метод в своем базовом классе, из которого я расширяю свои тестовые классы, и в этом случае это имеет смысл. Название класса здесь будет неправильным.
robert.egginton
Я сделал точно такой же кусок кода, основанный на teastburn xD
Nebulosar
23

Я хотел бы предложить небольшое изменение для getMethod (), определенного в ответе Укельмана .

Эта версия изменяет getMethod (), удаляя жестко запрограммированные значения и немного упрощая использование. Я рекомендую добавить его в ваш класс PHPUnitUtil, как в примере ниже, или в ваш класс, расширяющий PHPUnit_Framework_TestCase (или, я полагаю, глобально в ваш файл PHPUnitUtil).

Так как MyClass создается в любом случае, а ReflectionClass может принимать строку или объект ...

class PHPUnitUtil {
    /**
     * Get a private or protected method for testing/documentation purposes.
     * How to use for MyClass->foo():
     *      $cls = new MyClass();
     *      $foo = PHPUnitUtil::getPrivateMethod($cls, 'foo');
     *      $foo->invoke($cls, $...);
     * @param object $obj The instantiated instance of your class
     * @param string $name The name of your private/protected method
     * @return ReflectionMethod The method you asked for
     */
    public static function getPrivateMethod($obj, $name) {
      $class = new ReflectionClass($obj);
      $method = $class->getMethod($name);
      $method->setAccessible(true);
      return $method;
    }
    // ... some other functions
}

Я также создал функцию псевдонима getProtectedMethod () для явного определения ожидаемого, но это зависит от вас.

Ура!

teastburn
источник
+1 за использование API класса отражения.
Билл Ортелл
10

Я думаю, что troelskn близко. Я бы сделал это вместо этого:

class ClassToTest
{
   protected function testThisMethod()
   {
     // Implement stuff here
   }
}

Затем реализуйте что-то вроде этого:

class TestClassToTest extends ClassToTest
{
  public function testThisMethod()
  {
    return parent::testThisMethod();
  }
}

Затем вы запускаете свои тесты для TestClassToTest.

Должна быть возможность автоматически генерировать такие классы расширений путем анализа кода. Я не удивлюсь, если PHPUnit уже предлагает такой механизм (хотя я не проверял).

Майкл Джонсон
источник
Хех ... кажется, я говорю, используйте ваш третий вариант :)
Майкл Джонсон
2
Да, это именно мой третий вариант. Я уверен, что PHPUnit не предлагает такой механизм.
GrGr
Это не будет работать, вы не можете переопределить защищенную функцию с публичной функцией с тем же именем.
Коен.
Я могу ошибаться, но я не думаю, что этот подход может работать. PHPUnit (насколько я когда-либо использовал) требует, чтобы ваш тестовый класс расширял другой класс, который обеспечивает реальную функциональность тестирования. Если нет способа обойти это, я не уверен, что смогу увидеть, как можно использовать этот ответ. phpunit.de/manual/current/en/…
Cypher
1
К вашему сведению, это работает только для защищенных методов, а не для частных
Sliq
5

Я собираюсь бросить свою шляпу в кольцо здесь:

Я использовал взлом __call с разной степенью успеха. Альтернативой, которую я придумал, было использование шаблона Visitor:

1: создать класс stdClass или пользовательский класс (для принудительного применения типа)

2: начните с требуемого метода и аргументов

3: убедитесь, что ваша SUT имеет метод acceptVisitor, который будет выполнять метод с аргументами, указанными в классе посещения

4: введите его в класс, который вы хотите проверить

5: SUT внедряет результат операции в посетителя

6: применить ваши условия теста к атрибуту результата посетителя

sunwukung
источник
1
+1 за интересное решение
jsh
5

Вы действительно можете использовать __call () для доступа к защищенным методам. Чтобы иметь возможность проверить этот класс

class Example {
    protected function getMessage() {
        return 'hello';
    }
}

вы создаете подкласс в ExampleTest.php:

class ExampleExposed extends Example {
    public function __call($method, array $args = array()) {
        if (!method_exists($this, $method))
            throw new BadMethodCallException("method '$method' does not exist");
        return call_user_func_array(array($this, $method), $args);
    }
}

Обратите внимание, что метод __call () никоим образом не ссылается на класс, поэтому вы можете скопировать вышеприведенное для каждого класса с защищенными методами, которые вы хотите протестировать, и просто изменить объявление класса. Возможно, вы сможете поместить эту функцию в общий базовый класс, но я не пробовал.

Теперь сам контрольный пример отличается только тем, где вы создаете объект для тестирования, меняя его в ExampleExposed for Example.

class ExampleTest extends PHPUnit_Framework_TestCase {
    function testGetMessage() {
        $fixture = new ExampleExposed();
        self::assertEquals('hello', $fixture->getMessage());
    }
}

Я считаю, что PHP 5.3 позволяет вам использовать отражение для непосредственного изменения доступности методов, но я предполагаю, что вам придется делать это для каждого метода в отдельности.

Дэвид Харкнесс
источник
1
Реализация __call () прекрасно работает! Я пытался проголосовать, но я отменил свой голос до тех пор, пока я не протестировал этот метод, и теперь мне не разрешают голосовать из-за ограничения по времени в SO.
Адам Франко
call_user_method_array()Начиная с PHP 4.1.0, эта функция устарела ... используйте call_user_func_array(array($this, $method), $args)вместо этого. Обратите внимание, что если вы используете PHP 5.3.2+, вы можете использовать Reflection для получения доступа к защищенным / приватным методам и атрибутам
nuqqsa
@nuqqsa - Спасибо, я обновил свой ответ. С тех пор я написал универсальный Accessibleпакет, который использует отражение, чтобы позволить тестам получить доступ к закрытым / защищенным свойствам и методам классов и объектов.
Дэвид Харкнесс
Этот код не работает для меня в PHP 5.2.7 - метод __call не вызывается для методов, определенных базовым классом. Я не могу найти это документированным, но я предполагаю, что это поведение было изменено в PHP 5.3 (где я подтвердил, что это работает).
Рассел Дэвис
@Russell - __call()вызывается, только если вызывающая сторона не имеет доступа к методу. Поскольку класс и его подклассы имеют доступ к защищенным методам, вызовы к ним не будут проходить __call(). Можете ли вы опубликовать свой код, который не работает в 5.2.7, в новом вопросе? Я использовал вышеизложенное в 5.2 и только перешел на использование отражения в 5.3.2.
Дэвид Харкнесс
2

Я предлагаю следующий обходной путь для обходного пути / идеи "Henrik Paul" :)

Вы знаете имена частных методов вашего класса. Например, они похожи на _add (), _edit (), _delete () и т. Д.

Следовательно, когда вы хотите проверить это с точки зрения модульного тестирования, просто вызовите частные методы, добавив префикс и / или суффикс какого-нибудь общего слова (например, _addPhpunit), чтобы при вызове метода __call () (так как метод _addPhpunit () не существует) класса владельца, вы просто помещаете необходимый код в метод __call (), чтобы удалить префиксное / суффиксное слово / с (Phpunit), а затем вызвать оттуда этот выведенный приватный метод. Это еще одно хорошее использование магических методов.

Попробуйте это.

Анируд Зала
источник