Program Tip

PHPUnit으로 개인 메서드 모의

programtip 2020. 11. 16. 22:02
반응형

PHPUnit으로 개인 메서드 모의


PHPUnit을 사용하여 클래스 내부의 개인 메서드를 모의하는 것에 대한 질문이 있습니다. 예를 들어 소개하겠습니다.

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

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

public 함수의 더 많은 코드 부분 을 테스트하기 위해 private 메서드의 결과를 어떻게 스텁 할 수 있습니까 ?

여기에서 부분적으로 읽기 해결


일반적으로 개인 및 보호 방법을 직접 테스트하거나 모의하지 않습니다.

테스트하려는 것은 클래스 공용 API입니다. 다른 모든 것은 클래스의 구현 세부 사항이며 변경해도 테스트를 "중단"해서는 안됩니다.

또한 공개 API를 호출하여 실행할 수없는 코드가 클래스에있을 수 있기 때문에 "100 % 코드 커버리지를 얻을 수 없음"을 발견 할 때도 도움이됩니다.


일반적으로이 작업을 원하지 않습니다.

그러나 수업이 다음과 같은 경우 :

class a {

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

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

"무작위"함수가 전역 상태이고이를 테스트 할 수 없기 때문에 c ()를 모의하고 싶을 필요가 있음을 알 수 있습니다.

"깨끗한? / verbose? / overcomplicated-maybe? / i-like-it-usually"솔루션

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 ()"이기 때문입니다.


"Pragmatic Unit Testing"책을 인용하려면 :

"일반적으로 테스트를 위해 캡슐화를 깨고 싶지는 않습니다 (또는 엄마가"개인 정보를 노출하지 마십시오! "라고 말했듯이). 대부분의 경우 클래스를 테스트 할 수 있어야합니다. 비공개 또는 보호 된 액세스 뒤에 숨겨져있는 중요한 기능이있는 경우 다른 클래스가 빠져 나가기 위해 고군분투하고 있다는 경고 신호일 수 있습니다. "


좀 더: Why you don't want test private methods.


개인 메서드테스트 할 수 있지만이 메서드의 실행을 시뮬레이션 (모의) 할 수는 없습니다.

또한 리플렉션을 사용하면 개인 메서드를 보호 된 메서드 나 공용 메서드로 변환 할 수 없습니다. setAccessible 은 원래 메서드를 호출하는 것만 허용합니다.

또는 runkit사용 하여 개인 메소드의 이름을 바꾸고 "새 구현"을 포함 할 수 있습니다 . 그러나 이러한 기능은 실험적이므로 사용하지 않는 것이 좋습니다.


당신이 사용할 수있는 반사 하고 setAccessible()당신이 당신이 개인 방법에서 원하는 반환하는 방식으로 개체의 내부 상태를 설정할 수 있도록 당신의 검사 결과에있다. PHP 5.3.2에 있어야합니다.

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

protected method의 모의를 얻을 수 있으므로 C를 protected로 변환 할 수 있다면이 코드가 도움이 될 것입니다.

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

내 경우에이 범용 클래스를 생각해 냈습니다.

/**
 * @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 클래스에서 호출 된 속성 / 메서드를 캐싱하여 개선 할 수 있습니다.


한 가지 옵션은 c() protected대신 만들고 private하위 클래스를 만들고 재정의하는 것 c()입니다. 그런 다음 하위 클래스로 테스트하십시오. 또 다른 옵션은 c()주입 할 수있는 다른 클래스로 리팩토링 하는 A입니다 (이를 종속성 주입이라고 함). 그런 다음 c()단위 테스트에서 의 모의 구현으로 테스트 인스턴스를 삽입 하십시오.


다른 해결책은 개인 방법을 보호 된 방법으로 변경 한 다음 모의하는 것입니다.

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

$response = $myMockObject->myPublicMethod();

where myPublicMethod calls myProtectedMethod. Unfortunately we can not do this with private methods since setMethods can not find a private method where as it can find a protected method


You can use anonymous classes using PHP 7.

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

In prior versions of PHP you can make a test class extending the base class.

참고URL : https://stackoverflow.com/questions/5937845/mock-private-method-with-phpunit

반응형