Mock prywatna metoda z PHPUnit

Mam pytanie dotyczące używania PHPUnit do wyśmiewania prywatnej metody wewnątrz klasy. Pozwól, że przedstawię przykład:

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

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

Jak mogę sprawdzić wynik metody prywatnej, aby przetestować trochę więcej kodu części funkcji publicznej.

Rozwiązane częściowo czytanie tutaj

Author: David Harkness, 2011-05-09

10 answers

Zazwyczaj po prostu nie testujesz lub nie wyśmiewasz prywatnych i chronionych metod directy.

To, co chcesz przetestować, to publiczne API twojej klasy. Wszystko inne jest szczegółem implementacji dla twojej klasy i nie powinno "łamać" testów, jeśli go zmienisz.

To również pomaga, gdy zauważysz, że "nie możesz uzyskać 100% pokrycia kodu", ponieważ możesz mieć kod w swojej klasie, którego nie możesz wykonać, wywołując publiczne API.


Zazwyczaj nie chcesz robić to

Ale jeśli twoja klasa wygląda tak:

class a {

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

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

Widzę potrzebę wymazywania c (), ponieważ funkcja "random" jest stanem globalnym i nie można tego przetestować.

"czyste?/ gadatliwy?/ overcomplicated-maybe?/ I-like-it-usually" rozwiązanie

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

Teraz nie ma już potrzeby wyśmiewania "c ()", ponieważ nie zawiera żadnych globali i można je ładnie przetestować.


Jeśli nie chcesz lub nie możesz usunąć stanu globalnego z funkcji prywatnej (źle thing bad reality or you definition of bad might be different) that you can test against the mock.

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

I wykonaj testy na tej mocku, ponieważ jedyną funkcją, którą wyjmujesz / mock, jest " c ()".


Cytując książkę "pragmatyczne testy jednostkowe":

"ogólnie rzecz biorąc, nie chcesz łamać żadnej enkapsulacji ze względu na testowanie (lub jak mawiała mama, "nie ujawniaj swoich privatów!"). W większości przypadków powinieneś być w stanie przetestować klasę przez korzystanie z jej publicznych metod. Jeśli istnieje znaczna funkcjonalność ukryta za dostępem prywatnym lub chronionym, może to być znak ostrzegawczy, że inna klasa stara się wydostać."


Some more: Why you don't want test private methods.

 92
Author: edorian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-30 13:45:13

Możesz przetestować prywatne metody ale nie można symulować (naśladować) działania tych metod.

Ponadto, odbicie nie pozwala na konwersję metody prywatnej na metodę chronioną lub publiczną. setAccessible pozwala tylko na wywołanie oryginalnej metody.

Alternatywnie możesz użyć runkit do zmiany nazw metod prywatnych i dodania "nowej implementacji". Jednak funkcje te są eksperymentalne i ich użycie nie jest polecam.

 27
Author: doctore,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-20 12:01:39

Możesz użyć reflection i setAccessible() w Twoich testach, aby umożliwić Ci ustawienie wewnętrznego stanu obiektu w taki sposób, że zwróci to, co chcesz od metody prywatnej. Musisz być na PHP 5.3.2.

$fixture = new MyClass(...);
$reflector = new ReflectionProperty('MyClass', 'myPrivateProperty');
$reflector->setAccessible(true);
$reflector->setValue($fixture, 'value');
// test $fixture ...
 25
Author: David Harkness,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-10 01:07:10

Mock of protected method, so if you can convert C to protected then this code will help.

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

    $response = $mock->B();
To na pewno zadziała , u mnie zadziałało . Następnie do pokrycia chronionej metody C można użyć klas reflection.
 16
Author: Archit Rastogi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-08 01:46:42

Zakładając, że musisz przetestować $myClass- > privateMethodX ($arg1, $arg2), możesz to zrobić z odbiciem:

$class = new ReflectionClass ($myClass);
$method = $class->getMethod ('privateMethodX');
$method->setAccessible(true);
$output = $method->invoke ($myClass, $arg1, $arg2);
 12
Author: Edson Medina,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-17 10:31:37

Oto odmiana innych odpowiedzi, które mogą być użyte do wykonania takich połączeń w jednej linii:

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);
}
 10
Author: Mark McEver,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-02 15:25:01

Wymyśliłem tę klasę ogólnego przeznaczenia dla mojej sprawy:

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

Z tą klasą możesz teraz łatwo i przejrzyście wyzwalać wszystkie prywatne funkcje / pola na dowolnym obiekcie.

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

Jeśli wydajność jest ważna, można ją poprawić poprzez buforowanie właściwości / metod wywołanych w klasie Liberator.

 7
Author: Torge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-19 13:09:48

Jedną z opcji byłoby c() protected zamiast private, a następnie podklasuj i nadpisuj c(). Następnie przetestuj swoją podklasę. Inną opcją byłoby przekształcenie c() do innej klasy, którą można wprowadzić do A (nazywa się to iniekcją zależności). Następnie wstrzyknij instancję testową z przykładową implementacją c() W teście jednostkowym.

 6
Author: Asaph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-09 13:58:00

Alternatywnym rozwiązaniem jest zmiana metody prywatnej na chronioną, a następnie mock.

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

$response = $myMockObject->myPublicMethod();

Gdzie myPublicMethod wywołuje myProtectedMethod. Niestety nie możemy tego zrobić z prywatnymi metodami, ponieważ setMethods nie możemy znaleźć prywatnej metody, gdzie można znaleźć chronioną metodę

 1
Author: Thellimist,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-19 13:10:11

Możesz używać anonimowych klas używając PHP 7.

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

W poprzednich wersjach PHP można utworzyć klasę testową rozszerzającą klasę bazową.

 0
Author: jgmjgm,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-06-13 17:44:11