Подтвердить что ты не робот

Макет частного метода с помощью PHPUnit

У меня есть вопрос об использовании PHPUnit для издевательства частного метода внутри класса. Позвольте мне привести пример:

class A {
  public function b() { 
    // some code
    $this->c(); 
    // some more code
  }

  private function c(){ 
    // some code
  }
}

Как я могу заглушить результат частного метода, чтобы проверить еще часть кода публичной функции.

Решено частично читать здесь

4b9b3361

Ответ 1

Обычно вы просто не проверяете и не издеваетесь над личными и защищенными методами directy.

То, что вы хотите протестировать, - это общедоступный API вашего класса. Все остальное - это деталь реализации для вашего класса и не должна "ломать" ваши тесты, если вы меняете его.

Это также помогает вам, когда вы заметите, что "не можете получить 100% -ный охват кода", потому что у вас может быть код в вашем классе, который вы не можете выполнить, вызывая открытый API.


Обычно вы не хотите этого делать

Но если ваш класс выглядит так:

class a {

    public function b() {
        return 5 + $this->c();
    }

    private function c() {
        return mt_rand(1,3);
    }
}

Я вижу необходимость хотеть издеваться над c(), поскольку "случайная" функция является глобальным состоянием, и вы не можете ее проверить.

"clean?/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.

Ответ 2

Вы можете проверить приватные методы но вы не можете имитировать (издеваться) над запуском этих методов.

Кроме того, отражение не позволяет преобразовать частный метод в защищенный или открытый метод. setAccessible позволяет только вызывать оригинальный метод.

В качестве альтернативы вы можете использовать runkit для переименования частных методов и включить "новую реализацию". Однако эти функции являются экспериментальными, и их использование не рекомендуется.

Ответ 3

Вы можете использовать reflection и setAccessible() в своих тестах, чтобы вы могли установить внутреннее состояние вашего объекта таким образом, чтобы оно возвращало то, что вы хотите, из частного метода. Вам нужно быть на PHP 5.3.2.

$fixture = new MyClass(...);
$reflector = new ReflectionProperty('MyClass', 'myPrivateProperty');
$reflector->setAccessible(true);
$reflector->setValue($fixture, 'value');
// test $fixture ...

Ответ 4

Вы можете получить макет защищенного метода, поэтому, если вы можете конвертировать C в защищенный, то этот код поможет.

 $mock = $this->getMockBuilder('A')
                  ->disableOriginalConstructor()
                  ->setMethods(array('C'))
                  ->getMock();

    $response = $mock->B();

Это определенно сработает, это сработало для меня. Затем Для покрытия защищенного метода C вы можете использовать классы отражения.

Ответ 5

Предполагая, что вам нужно проверить $myClass- > privateMethodX ($ arg1, $arg2), вы можете сделать это с отражением:

$class = new ReflectionClass ($myClass);
$method = $class->getMethod ('privateMethodX');
$method->setAccessible(true);
$output = $method->invoke ($myClass, $arg1, $arg2);

Ответ 6

Здесь варианты других ответов, которые могут быть использованы для того, чтобы сделать такие вызовы одной строкой:

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

Ответ 7

Я придумал этот класс общего назначения для моего случая:

/**
 * @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.

Ответ 8

Один из вариантов - сделать c() protected вместо private, а затем подклассом и переопределить c(). Затем проверьте свой подкласс. Другой вариант - реорганизовать c() в другой класс, который вы можете ввести в A (это называется инъекцией зависимостей). Затем добавьте тестовый экземпляр с макетной реализацией c() в unit test.

Ответ 9

Альтернативным решением является изменение вашего частного метода на защищенный метод, а затем макет.

$myMockObject = $this->getMockBuilder('MyMockClass')
        ->setMethods(array('__construct'))
        ->setConstructorArgs(array("someValue", 5))
        ->setMethods(array('myProtectedMethod'))
        ->getMock();

$response = $myMockObject->myPublicMethod();

где myPublicMethod вызывает myProtectedMethod. К сожалению, мы не можем сделать это с помощью частных методов, поскольку setMethods не может найти частный метод, где он может найти защищенный метод

Ответ 10

Вы можете использовать анонимные классы, используя PHP 7.

$mock = new class Concrete {
    private function bob():void
    {
    }
};

В предыдущих версиях PHP вы могли создать тестовый класс, расширяющий базовый класс.