Рекомендации по проверке защищенных методов с помощью PHPUnit

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

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

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

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

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

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

Ответ 1

Если вы используете 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(...));
  ...
}

Ответ 2

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

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

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

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

Ответ 3

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

Ответ 4

Я хотел бы предложить небольшую вариацию getMethod(), определенную в ответ uckelman.

Эта версия изменяет 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(), чтобы быть явным, что ожидалось, но это до вас.

Ура!

Ответ 5

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

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

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

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

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

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

Ответ 6

Вы действительно можете использовать __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 для примера.

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

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

Ответ 7

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

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

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

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

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

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

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

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

Ответ 8

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

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

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

Попробуйте.