Ложный приватный метод с PHPUnit
У меня есть вопрос об использовании PHPUnit для насмешки частного метода внутри класса. Позвольте мне представить пример:
class A {
public function b() {
// some code
$this->c();
// some more code
}
private function c(){
// some code
}
}
Как я могу заглушить результат приватного метода, чтобы протестировать еще часть кода публичной функции.
Решено частично читая здесь
11 ответов
Обычно вы просто не тестируете и не высмеиваете приватные и защищенные методы напрямую.
То, что вы хотите проверить, это публичный API вашего класса. Все остальное является деталью реализации для вашего класса и не должно "ломать" ваши тесты, если вы измените это.
Это также поможет вам, когда вы заметите, что "вы не можете получить 100% покрытие кода", потому что в вашем классе может быть код, который вы не можете выполнить, вызвав публичный API.
Вы обычно не хотите этого делать
Но если ваш класс выглядит так:
class a {
public function b() {
return 5 + $this->c();
}
private function c() {
return mt_rand(1,3);
}
}
я вижу необходимость хотеть макетировать c(), так как "случайная" функция является глобальным состоянием, и вы не можете это проверить.
"Чистое?/ Многословное?/ Слишком сложное, может быть,?/ I-like-it-обычно" решение
class a {
public function __construct(RandomGenerator $foo) {
$this->foo = $foo;
}
public function b() {
return 5 + $this->c();
}
private function c() {
return $this->foo->rand(1,3);
}
}
теперь больше нет необходимости использовать "c()", так как он не содержит глобальных переменных, и вы можете хорошо протестировать.
Если вы не хотите делать или не можете удалить глобальное состояние из вашей частной функции (плохая вещь плохая реальность или ваше определение плохой может отличаться), которую вы можете проверить на предмет имитации.
// maybe set the function protected for this to work
$testMe = $this->getMock("a", array("c"));
$testMe->expects($this->once())->method("c")->will($this->returnValue(123123));
и запустите ваши тесты против этой ложной проверки, поскольку единственная функция, которую вы убираете / смоделируете, это "c()".
Цитировать книгу "Прагматическое модульное тестирование":
"В общем, вы не хотите нарушать инкапсуляцию ради тестирования (или, как говорила мама," не выставляйте своих рядовых!"). В большинстве случаев вы должны иметь возможность протестировать класс осуществляя его общедоступные методы. Если за частным или защищенным доступом скрыты значительные функциональные возможности, это может быть предупреждением о том, что существует другой класс, пытающийся выбраться ".
Еще немного: Why you don't want test private methods.
Вы можете использовать отражение и setAccessible()
в ваших тестах, чтобы позволить вам установить внутреннее состояние вашего объекта таким образом, чтобы он возвращал то, что вы хотите от закрытого метода. Вам нужно быть на PHP 5.3.2.
$fixture = new MyClass(...);
$reflector = new ReflectionProperty('MyClass', 'myPrivateProperty');
$reflector->setAccessible(true);
$reflector->setValue($fixture, 'value');
// test $fixture ...
Вы можете протестировать закрытые методы, но не можете смоделировать (смоделировать) выполнение этих методов.
Кроме того, отражение не позволяет преобразовывать закрытый метод в защищенный или публичный метод. setAccessible позволяет только вызывать оригинальный метод.
В качестве альтернативы вы можете использовать runkit для переименования приватных методов и включения "новой реализации". Однако эти функции являются экспериментальными, и их использование не рекомендуется.
Вы можете получить макет защищенного метода, так что если вы можете конвертировать C в защищенный, тогда этот код поможет.
$mock = $this->getMockBuilder('A')
->disableOriginalConstructor()
->setMethods(array('C'))
->getMock();
$response = $mock->B();
Это точно сработает, у меня сработало. Тогда для покрытия защищенного метода C вы можете использовать классы отражения.
Предполагая, что вам нужно протестировать $myClass->privateMethodX($arg1, $arg2), вы можете сделать это с помощью отражения:
$class = new ReflectionClass ($myClass);
$method = $class->getMethod ('privateMethodX');
$method->setAccessible(true);
$output = $method->invoke ($myClass, $arg1, $arg2);
Одним из вариантов будет сделать c()
protected
вместо private
а затем подкласс и переопределить c()
, Затем проверьте с вашим подклассом. Другим вариантом будет рефакторинг c()
в другой класс, который вы можете ввести в A
(это называется внедрением зависимости). А затем внедрить тестовый экземпляр с фиктивной реализацией c()
в вашем модульном тесте.
Вот вариант других ответов, которые можно использовать для таких вызовов в одну строку:
public function callPrivateMethod($object, $methodName)
{
$reflectionClass = new \ReflectionClass($object);
$reflectionMethod = $reflectionClass->getMethod($methodName);
$reflectionMethod->setAccessible(true);
$params = array_slice(func_get_args(), 2); //get all the parameters after $methodName
return $reflectionMethod->invokeArgs($object, $params);
}
Я придумал этот класс общего назначения для моего случая:
/**
* @author Torge Kummerow
*/
class Liberator {
private $originalObject;
private $class;
public function __construct($originalObject) {
$this->originalObject = $originalObject;
$this->class = new ReflectionClass($originalObject);
}
public function __get($name) {
$property = $this->class->getProperty($name);
$property->setAccessible(true);
return $property->getValue($this->originalObject);
}
public function __set($name, $value) {
$property = $this->class->getProperty($name);
$property->setAccessible(true);
$property->setValue($this->originalObject, $value);
}
public function __call($name, $args) {
$method = $this->class->getMethod($name);
$method->setAccessible(true);
return $method->invokeArgs($this->originalObject, $args);
}
}
С помощью этого класса вы теперь можете легко и прозрачно освобождать все частные функции / поля любого объекта.
$myObject = new Liberator(new MyObject());
/* @var $myObject MyObject */ //Usefull for code completion in some IDEs
//Writing to a private field
$myObject->somePrivateField = "testData";
//Reading a private field
echo $myObject->somePrivateField;
//calling a private function
$result = $myObject->somePrivateFunction($arg1, $arg2);
Если производительность важна, ее можно улучшить, кэшируя свойства / методы, вызываемые в классе Liberator.
Альтернативное решение состоит в том, чтобы изменить свой закрытый метод на защищенный, а затем смоделировать.
$myMockObject = $this->getMockBuilder('MyMockClass')
->setMethods(array('__construct'))
->setConstructorArgs(array("someValue", 5))
->setMethods(array('myProtectedMethod'))
->getMock();
$response = $myMockObject->myPublicMethod();
где myPublicMethod
звонки myProtectedMethod
, К сожалению, мы не можем сделать это с помощью частных методов, так как setMethods
не могу найти приватный метод, где как можно найти защищенный метод
Когда целевой класс не является ни статическим, ни окончательным, я решаю эту проблему способом, близким к @jgmjgm, анонимно и локально расширяя исходный класс и, следовательно, не тестируя один и тот же метод несколько раз в разных тестовых случаях:
$extent = new class ($args) extends A {
private function c() {
return $hardCodedValue;
}
};
Затем я могу проверить поведение
$extent->b()
без повторного выполнения потенциально тяжелого
c()
метод.
Вы можете использовать анонимные классы, используя PHP 7.
$mock = new class Concrete {
private function bob():void
{
}
};
В предыдущих версиях PHP можно было создать тестовый класс, расширяющий базовый класс.