PHPUnit으로 보호 된 메소드를 테스트하는 모범 사례


287

개인 메소드 정보 를 테스트합니까? 에 대한 토론을 찾았습니다 .

일부 클래스에서는 보호 된 메소드를 갖고 싶지만 테스트하도록 결정했습니다. 이러한 방법 중 일부는 정적이고 짧습니다. 대부분의 공개 메소드는이 메소드를 사용하므로 나중에 테스트를 안전하게 제거 할 수 있습니다. 그러나 TDD 접근 방식으로 시작하고 디버깅을 피하기 위해 실제로 테스트하고 싶습니다.

나는 다음을 생각했다.

  • 답변에 조언 된 방법 객체 는 이것에 대해 과도한 것으로 보입니다.
  • 공개 방법으로 시작하고 더 높은 수준의 테스트로 코드 적용 범위를 지정하면 보호를 설정하고 테스트를 제거하십시오.
  • 보호 가능한 메소드를 공개하는 테스트 가능한 인터페이스로 클래스 상속

가장 좋은 방법은 무엇입니까? 다른 것이 있습니까?

JUnit은 자동으로 보호 된 메소드를 공개로 변경하는 것으로 보이지만 자세히 살펴 보지는 않았습니다. PHP는 리플렉션을 통해 이것을 허용하지 않습니다 .


두 가지 질문 : 1. 왜 수업에서 공개하지 않는 기능 테스트를 귀찮게해야합니까? 2. 테스트해야한다면 왜 비공개입니까?
nad2000

2
아마도 그는 사유 재산이 올바르게 설정되어 있는지 테스트하고 싶고 세터 기능 만 사용하여 테스트하는 유일한 방법은 사유 재산을 공개하고 데이터를 확인하는 것입니다.
AntonioCS

4
따라서 이것은 토론 스타일이므로 건설적이지 않습니다. 다시 :)
mlvljr

72
사이트의 규칙에 따라 호출 할 수 있지만 "건설적이지 않다"고 부르는 것은 모욕입니다.
Andy V

1
@Visser, 그것은 모욕이다;)
Pacerier

답변:


417

PHPUnit과 함께 PHP5 (> = 5.3.2)를 사용하는 경우 테스트를 실행하기 전에 리플렉션을 사용하여 개인용 및 보호 된 방법을 공개로 설정하여 테스트 할 수 있습니다.

protected static function getMethod($name) {
  $class = new ReflectionClass('MyClass');
  $method = $class->getMethod($name);
  $method->setAccessible(true);
  return $method;
}

public function testFoo() {
  $foo = self::getMethod('foo');
  $obj = new MyClass();
  $foo->invokeArgs($obj, array(...));
  ...
}

27
sebastians 블로그에 대한 링크를 인용하자면 : "그러므로 보호 된 속성과 개인 속성 및 방법을 테스트 할 수 있다고해서 이것이"좋은 것 "이라는 의미는 아닙니다." -명심하십시오
edorian

10
나는 그것에 대해 논쟁 할 것이다. 작동하는 데 보호 또는 개인 방법이 필요하지 않은 경우 테스트하지 마십시오.
uckelman

10
명확히하기 위해 PHPUnit을 사용할 필요가 없습니다. 또한 SimpleTest 또는 무엇이든 사용할 수 있습니다. PHPUnit에 의존하는 답변은 없습니다.
Ian Dunn

84
보호 / 개인 구성원을 직접 테스트해서는 안됩니다. 그것들은 클래스의 내부 구현에 속하며 테스트와 결합해서는 안됩니다. 이것은 리팩토링을 불가능하게하며 결국 테스트 할 대상을 테스트하지 않습니다. 공용 메소드를 사용하여 간접적으로 테스트해야합니다. 이 과정이 어렵다면 클래스 구성에 문제가 있는지 확인하고 더 작은 클래스로 분리해야합니다. 수업은 테스트를위한 블랙 박스 여야합니다. 무언가를 던져서 무언가를 되 찾으면됩니다.
gphilip

24
@gphilip 나에게 protected메소드는 퍼블릭 API의 일부입니다. 왜냐하면 제 3 자 클래스가 그것을 확장하고 마술없이 사용할 수 있기 때문 입니다. 따라서 private메서드 만 직접 테스트되지 않는 메서드 범주에 속 한다고 생각합니다 . protected그리고 public직접 테스트해야합니다.
Filip Halaxa

48

당신은 이미 알고있는 것처럼 보이지만 어쨌든 나는 그것을 다시 언급 할 것입니다. 보호 된 방법을 테스트해야하는 경우 나쁜 신호입니다. 단위 테스트의 목적은 클래스의 인터페이스를 테스트하는 것이며 보호 된 메소드는 구현 세부 사항입니다. 그것은 말이되는 경우가 있습니다. 상속을 사용하면 수퍼 클래스가 서브 클래스에 대한 인터페이스를 제공하는 것으로 볼 수 있습니다. 따라서 여기서는 보호 된 방법을 테스트해야합니다 (그러나 절대 개인용은 아닙니다 ). 이에 대한 해결책은 테스트 목적으로 서브 클래스를 작성하고이를 사용하여 메소드를 노출시키는 것입니다. 예 :

class Foo {
  protected function stuff() {
    // secret stuff, you want to test
  }
}

class SubFoo extends Foo {
  public function exposedStuff() {
    return $this->stuff();
  }
}

상속을 항상 컴포지션으로 바꿀 수 있습니다. 코드를 테스트 할 때는 일반적으로이 패턴을 사용하는 코드를 다루기가 훨씬 쉬우므로 해당 옵션을 고려할 수 있습니다.


2
stuff ()를 public으로 직접 구현하고 parent :: stuff ()를 반환 할 수 있습니다. 내 답변을 참조하십시오. 오늘 너무 빨리 읽는 것 같습니다.
Michael Johnson

네가 옳아; 보호 된 메소드를 공용 메소드로 변경하는 것은 유효합니다.
troelskn

따라서이 코드는 세 번째 옵션과 "상속을 컴포지션으로 항상 바꿀 수 있습니다."를 제안합니다. 내 첫 번째 옵션 또는 refactoring.com/catalog/replaceInheritanceWithDelegation.html
GrGr

34
나는 그것이 나쁜 징조라는 것에 동의하지 않습니다. TDD와 단위 테스트를 차별화 해 봅시다. 단위 테스트는 개별 메소드 imo를 테스트해야합니다. 왜냐하면 이러한 메소드는 단위이며 단위 테스트의 공용 메소드가 단위 테스트의 이점과 동일한 방식으로 이익을 얻을 수 있기 때문입니다.
koen

36
보호 된 메소드 클래스 인터페이스의 일부이며 단순한 구현 세부 사항이 아닙니다. 보호 된 멤버의 요점은 하위 클래스 (자신의 사용자)가 보호 된 메서드를 클래스 설명 내에서 사용할 수 있도록하는 것입니다. 그것들은 분명히 테스트해야합니다.
BT

40

teastburn 이 올바른 접근 방식을 가지고 있습니다. 더 간단한 방법은 메소드를 직접 호출하고 답변을 반환하는 것입니다.

class PHPUnitUtil
{
  public static function callMethod($obj, $name, array $args) {
        $class = new \ReflectionClass($obj);
        $method = $class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($obj, $args);
    }
}

테스트에서 간단히 다음과 같이 호출 할 수 있습니다.

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );

1
이것은 좋은 예입니다. 감사합니다. 이 방법은 보호되지 않고 공개되어야합니까?
valk

좋은 지적. 실제로 테스트 클래스를 확장하는 기본 클래스 에서이 방법을 사용합니다.이 경우에는 의미가 있습니다. 클래스의 이름은 여기서 잘못되었을 것입니다.
robert.egginton

나는 teastburn xD를 기반으로 똑같은 코드를 만들었습니다
Nebulosar

23

uckelman의 답변에 정의 된 getMethod ()에 약간의 변형을 제안하고 싶습니다. .

이 버전은 하드 코딩 된 값을 제거하고 사용법을 약간 단순화하여 getMethod ()를 변경합니다. 아래 예제와 같이 PHPUnitUtil 클래스에 추가하거나 PHPUnit_Framework_TestCase-extending 클래스 (또는 PHPUnitUtil 파일의 전역)에 추가하는 것이 좋습니다.

MyClass가 어쨌든 인스턴스화되고 있으므로 ReflectionClass는 문자열이나 객체를 취할 수 있습니다 ...

class PHPUnitUtil {
    /**
     * Get a private or protected method for testing/documentation purposes.
     * How to use for MyClass->foo():
     *      $cls = new MyClass();
     *      $foo = PHPUnitUtil::getPrivateMethod($cls, 'foo');
     *      $foo->invoke($cls, $...);
     * @param object $obj The instantiated instance of your class
     * @param string $name The name of your private/protected method
     * @return ReflectionMethod The method you asked for
     */
    public static function getPrivateMethod($obj, $name) {
      $class = new ReflectionClass($obj);
      $method = $class->getMethod($name);
      $method->setAccessible(true);
      return $method;
    }
    // ... some other functions
}

또한 예상되는 것을 명시 적으로하기 위해 getProtectedMethod ()라는 별칭 함수를 만들었지 만 그게 당신에게 달려 있습니다.

건배!


리플렉션 클래스 API 사용시 +1
Bill Ortell

10

나는 troelskn이 가까이 있다고 생각합니다. 대신이 작업을 수행합니다.

class ClassToTest
{
   protected function testThisMethod()
   {
     // Implement stuff here
   }
}

그런 다음 다음과 같이 구현하십시오.

class TestClassToTest extends ClassToTest
{
  public function testThisMethod()
  {
    return parent::testThisMethod();
  }
}

그런 다음 TestClassToTest에 대해 테스트를 실행하십시오.

코드를 구문 분석하여 이러한 확장 클래스를 자동으로 생성 할 수 있어야합니다. PHPUnit이 이미 그러한 메커니즘을 제공하더라도 놀라지 않을 것입니다 (확인하지는 않았지만).


허 .. 세 번째 옵션을 사용하는 것 같습니다 :)
Michael Johnson

2
네, 이것이 제 세 번째 옵션입니다. PHPUnit은 그러한 메커니즘을 제공하지 않습니다.
GrGr

이 기능은 작동하지 않습니다. 보호 된 기능을 동일한 이름의 공용 기능으로 재정의 할 수 없습니다.
코엔.

틀릴 수도 있지만이 방법이 효과가 있다고 생각하지 않습니다. PHPUnit (필자가 사용한 한)은 테스트 클래스가 실제 테스트 기능을 제공하는 다른 클래스를 확장해야합니다. 주위에 방법이 없다면이 답변을 어떻게 사용할 수 있는지 잘 모르겠습니다. phpunit.de/manual/current/en/…
Cypher

1
참고로이 온라인 은 개인용이 아닌 보호 된 방법으로 작동 합니다
Sliq

5

모자를 고리에 넣을 것입니다.

나는 __call 핵을 다양한 수준의 성공으로 사용했습니다. 내가 생각해 낸 대안은 방문자 패턴을 사용하는 것입니다.

1 : stdClass 또는 사용자 정의 클래스를 생성하십시오 (유형을 시행하기 위해)

2 : 필수 메소드와 인수로이를 준비하십시오.

3 : SUT에 방문 클래스에 지정된 인수로 메소드를 실행하는 acceptVisitor 메소드가 있는지 확인하십시오.

4 : 시험하고자하는 수업에 주입

5 : SUT는 조작 결과를 방문자에게 주입합니다.

6 : 방문자의 결과 속성에 테스트 조건 적용


1
흥미로운 솔루션에 +1
jsh

5

실제로 __call ()을 일반적인 방식으로 사용하여 보호 된 메소드에 액세스 할 수 있습니다. 이 수업을 테스트 할 수 있도록

class Example {
    protected function getMessage() {
        return 'hello';
    }
}

ExampleTest.php에서 서브 클래스를 생성합니다 :

class ExampleExposed extends Example {
    public function __call($method, array $args = array()) {
        if (!method_exists($this, $method))
            throw new BadMethodCallException("method '$method' does not exist");
        return call_user_func_array(array($this, $method), $args);
    }
}

__call () 메서드는 어떤 식 으로든 클래스를 참조하지 않으므로 테스트하려는 보호 된 메서드를 사용하여 각 클래스에 대해 위의 내용을 복사하고 클래스 선언 만 변경할 수 있습니다. 이 함수를 공통 기본 클래스에 배치 할 수는 있지만 시도하지는 않았습니다.

이제 테스트 사례 자체는 테스트 할 개체를 구성하는 위치 만 다르고 ExampleExposed를 Example로 교체했습니다.

class ExampleTest extends PHPUnit_Framework_TestCase {
    function testGetMessage() {
        $fixture = new ExampleExposed();
        self::assertEquals('hello', $fixture->getMessage());
    }
}

PHP 5.3에서는 리플렉션을 사용하여 메소드의 접근성을 직접 변경할 수 있다고 생각하지만 각 메소드마다 개별적으로 변경해야한다고 가정합니다.


1
__call () 구현은 훌륭합니다! 투표를하려고했지만이 방법을 테스트 한 후에야 투표가 해제되었습니다. 이제 시간 제한으로 인해 투표가 허용되지 않습니다.
Adam Franco

call_user_method_array()함수는 PHP 4.1.0부터 더 이상 사용되지 않습니다 ... call_user_func_array(array($this, $method), $args)대신 사용하십시오. PHP 5.3.2 이상을 사용하는 경우 Reflection을 사용 하여 보호 / 개인 메소드 및 속성에 액세스
nuqqsa

@nuqqsa-감사합니다. 답변을 업데이트했습니다. 이후 Accessible리플렉션을 사용하여 테스트가 클래스 및 객체의 개인 / 보호 속성 및 메서드에 액세스 할 수 있도록 하는 일반 패키지를 작성했습니다 .
David Harkness

이 코드는 PHP 5.2.7에서 작동하지 않습니다. __call 메소드는 기본 클래스가 정의한 메소드에 대해 호출되지 않습니다. 문서화 된 것을 찾을 수는 없지만이 동작이 PHP 5.3에서 변경되었다고 생각합니다 (작동하는 곳을 확인했습니다).
Russell Davis

@Russell- __call()호출자가 메소드에 액세스 할 수없는 경우에만 호출됩니다. 클래스와 그 서브 클래스는 보호 된 메소드에 액세스 할 수 있으므로 호출 할 수 없습니다 __call(). 5.2.7에서 작동하지 않는 코드를 새 질문에 게시 할 수 있습니까? 위의 5.2에서 위의 내용을 사용했으며 5.3.2의 리플렉션 사용으로 만 이동했습니다.
David Harkness

2

"Henrik Paul"의 해결 방법 / 아이디어에 대한 다음 해결 방법을 제안합니다. :)

클래스의 개인 메소드 이름을 알고 있습니다. 예를 들어 _add (), _edit (), _delete () 등과 같습니다.

따라서 단위 테스트 측면에서 테스트하려는 경우 일반적인 접두사 및 접미사를 사용하여 개인 메소드를 호출하십시오. 경우 __call () 메서드가 호출 될 때 (_addPhpunit () 메서드가 호출되지 않으므로) 단어 (예 : _addPhpunit). 소유자 클래스의 경우 __call () 메소드에 필요한 코드를 삽입하여 접두사 / 접미사 단어 (Phpunit)를 제거한 다음 거기에서 개인 메소드를 추론합니다. 이것은 마술 방법의 또 다른 좋은 사용법입니다.

사용해보십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.