Ложный приватный метод с PHPUnit - PullRequest
65 голосов
/ 09 мая 2011

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

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

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

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

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

Ответы [ 10 ]

80 голосов
/ 09 мая 2011

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

То, что вы хотите проверить, - это публичный 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));

и запустите ваши тесты против этого макета, поскольку единственная функция, которую вы убираете / mock, это "c ()".


Цитировать книгу "Прагматическое модульное тестирование":

"В общем, вы не хотите нарушать инкапсуляцию ради тестирования (или, как говорила мама," не выставляйте своих рядовых! "). В большинстве случаев вы должны иметь возможность протестируйте класс, используя его открытые методы. Если за закрытым или защищенным доступом скрыта значительная функциональность, это может быть предупреждением о том, что существует другой класс, пытающийся выйти из него. "


Еще немного: Why you don't want test private methods.

25 голосов
/ 10 мая 2011

Вы можете проверить приватные методы , но вы не можете смоделировать (смоделировать) выполнение этих методов.

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

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

24 голосов
/ 10 мая 2011

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

$fixture = new MyClass(...);
$reflector = new ReflectionProperty('MyClass', 'myPrivateProperty');
$reflector->setAccessible(true);
$reflector->setValue($fixture, 'value');
// test $fixture ...
13 голосов
/ 08 августа 2016

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

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

    $response = $mock->B();

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

10 голосов
/ 17 августа 2011

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

$class = new ReflectionClass ($myClass);
$method = $class->getMethod ('privateMethodX');
$method->setAccessible(true);
$output = $method->invoke ($myClass, $arg1, $arg2);
9 голосов
/ 02 августа 2013

Вот вариант других ответов, которые можно использовать для таких вызовов в одну строку:

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);
}
6 голосов
/ 19 февраля 2016

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

/**
 * @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);
    }
}

С помощью этого класса вы теперь можете легко и прозрачно освобождать все частные функции / поля любого объекта.1006 * Если производительность важна, ее можно улучшить, кэшируя свойства / методы, вызываемые в классе Liberator.

5 голосов
/ 09 мая 2011

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

2 голосов
/ 19 июля 2016

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

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

$response = $myMockObject->myPublicMethod();

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

0 голосов
/ 13 июня 2019

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

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

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

...