Кращі практики тестування захищених методів за допомогою PHPUnit


287

Я знайшов дискусію на тестуванні ви тестуєте приватний метод інформативним.

Я вирішив, що в деяких класах я хочу мати захищені методи, але перевірити їх. Деякі з цих методів є статичними та короткими. Оскільки більшість публічних методів використовує їх, я, ймовірно, зможу безпечно зняти тести пізніше. Але для початку з підходу TDD та уникнення налагодження я дуже хочу перевірити їх.

Я подумав про наступне:

  • Метод Об'єкт, про який йдеться у відповіді, здається для цього непосильним.
  • Почніть з загальнодоступних методів, і коли покриття коду надається тестами вищого рівня, поверніть їх захищеними та видаліть тести.
  • Успадкуйте клас із тестовим інтерфейсом, що робить захищені методи відкритими

Яка найкраща практика? Чи є ще щось?

Здається, що JUnit автоматично змінює захищені методи, щоб бути відкритими, але я не мав глибшого погляду на це. PHP не дозволяє цього через рефлексію .


Два запитання: 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(...));
  ...
}

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

Зауважте, що завжди можна замінити спадщину складом. Під час тестування коду, як правило, набагато простіше мати справу з кодом, який використовує цю схему, тому ви можете розглянути цей варіант.


2
Ви можете просто безпосередньо реалізовувати речі () як загальнодоступні та повертати батько :: stuff (). Дивіться мою відповідь. Здається, я сьогодні занадто швидко читаю речі.
Майкл Джонсон

Ти маєш рацію; Дійсно змінити захищений метод на публічний.
troelskn

Тож код пропонує мій третій варіант і "Зауважте, що ви завжди можете замінити спадщину складом". йде в напрямку мого першого варіанту або refactoring.com/catalog/replaceInheritanceWithDelegation.html
GrGr

34
Я не згоден, що це поганий знак. Давайте зробимо різницю між TDD та Unit Testing. Тестування одиниць повинно перевірити приватні методи imo, оскільки це одиниці і отримали б користь так само, як і одиничні тестування, які мають публічне використання, виграють від одиничного тестування.
koen

36
Захищені методи є частиною інтерфейсу класу, вони не просто деталі реалізації. Вся суть захищених членів полягає в тому, що підкласи (користувачі самі) можуть використовувати ці захищені методи всередині класів exstions. Вони чітко потребують тестування.
BT

40

театральна опіка має правильний підхід. Ще простішим є виклик методу безпосередньо та повернення відповіді:

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

1
Це чудовий приклад, дякую. Метод повинен бути загальнодоступним, а не захищеним, чи не так?
valk

Гарна думка. Я фактично використовую цей метод у своєму базовому класі, з якого подовжую свої тестові класи, і в цьому випадку це має сенс. Ім'я класу тут було б неправильним.
robert.egginton

Я зробив такий самий фрагмент коду, що базується на тест-опіку xD
Небулосар

23

Я хотів би запропонувати невелику варіацію, щоб 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 (), щоб пояснити, що очікується, але це залежить від вас.

Ура!


+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
FYI цей
он-лайн

5

Я збираюся сюди кинути шапку на ринг:

Я використовував хак-__call зі змішаним ступенем успіху. Альтернатива, яку я придумав, - це використовувати шаблон відвідувачів:

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

2: першочергово, що потрібний метод та аргументи

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

4: введіть його в клас, який ви хочете протестувати

5: SUT вводить у відвідувач результат операції

6: застосуйте свої тестові умови до атрибута результату відвідувача


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 () працює чудово! Я спробував проголосувати, але я зняв свій голос до тих пір, поки не перевірив цей метод, і тепер мені не дозволяють голосувати через обмеження часу в ТА.
Адам Франко

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

Я пропоную наступне рішення для вирішення / ідеї "Генріка Пола" :)

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

Отже, коли ви хочете протестувати його з аспекту тестування одиниць, просто зателефонуйте до приватних методів за допомогою префіксації та / або суфіксації деяких загальних слова (наприклад, _addPhpunit), щоб при виклику методу __call () (оскільки метод _addPhpunit () не існує клас власника, ви просто введете необхідний код у метод __call (), щоб видалити префікс / суфікс слово / с (Phpunit), а потім викликати цей виведений приватний метод звідти. Це ще одне добре використання магічних методів.

Спробуй.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.