У меня есть вопрос об использовании PHPUnit для издевательства частного метода внутри класса. Позвольте мне привести пример:
class A { public function b() { // some code $this->c(); // some more code } private function c(){ // some code } }
Как я могу заглушить результат частного метода, чтобы проверить еще часть кода публичной функции.
Решено частично прочитать здесь
Обычно вы просто не проверяете и не издеваетесь над личными и защищенными методами directy.
То, что вы хотите протестировать, – это публичный API вашего класса. Все остальное является детализацией реализации для вашего класса и не должно «ломать» ваши тесты, если вы его измените.
Это также помогает вам, когда вы замечаете, что «не можете получить 100% -ный охват кода», потому что у вас может быть код в вашем классе, который вы не можете выполнить, вызывая открытый API.
Но если ваш класс выглядит так:
class a { public function b() { return 5 + $this->c(); } private function c() { return mt_rand(1,3); } }
я вижу, что вам нужно высмеять c (), поскольку «случайная» функция является глобальным состоянием, и вы не можете ее проверить.
«Чистый? / Verbose? / Overcomplicated-возможно? / 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));
и запускайте свои тесты против этого макета, поскольку единственная функция, которую вы вынимаете / mock, это «c ()».
Чтобы процитировать книгу «Прагматическое тестирование единицы»:
«В общем, вы не хотите нарушать инкапсуляцию ради тестирования (или, как говорила мама,« не выставляйте своих рядовых! »). В большинстве случаев вы должны иметь возможность протестировать класс используя свои общедоступные методы.Если есть значительная функциональность, которая скрыта за закрытым или защищенным доступом, это может быть предупреждающим знаком о том, что есть еще один класс, который пытается выйти ».
Еще несколько: Why you don't want test private methods.
Вы можете проверить частные методы, но вы не можете имитировать (макет) запуск этих методов.
Кроме того, отражение не позволяет преобразовать частный метод в защищенный или общедоступный метод. setAccessible позволяет вам вызывать оригинальный метод.
Кроме того, вы можете использовать runkit для переименования частных методов и включить «новую реализацию». Однако эти функции являются экспериментальными, и их использование не рекомендуется.
Вы можете использовать reflection и setAccessible()
в своих тестах, чтобы вы могли установить внутреннее состояние вашего объекта таким образом, чтобы он возвращал то, что вы хотите, из частного метода. Вам нужно быть на PHP 5.3.2.
$fixture = new MyClass(...); $reflector = new ReflectionProperty('MyClass', 'myPrivateProperty'); $reflector->setAccessible(true); $reflector->setValue($fixture, 'value'); // test $fixture ...
Вы можете получить mock защищенного метода, поэтому, если вы можете конвертировать 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);
Ниже приведены варианты других ответов, которые можно использовать для создания таких вызовов в одной строке:
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); }
Одним из вариантов было бы сделать c()
protected
вместо private
а затем подкласса и переопределить c()
. Затем проверьте свой подкласс. Другой вариант – рефакторинг c()
в другой класс, который вы можете ввести в A
(это называется инъекцией зависимостей). А затем вставьте тестовый экземпляр с макетной реализацией c()
в вашем модульном тесте.
Я придумал этот класс общего назначения для моего дела:
/** * @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
не может найти частный метод, где он может найти защищенный метод