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.