PHPUnit을 사용하여 보호된 메서드를 테스트하는 모범 사례
Do you test private method에 대한 논의는 유익했습니다.
일부 수업에서는 보호 방법을 사용하되 테스트하기로 결정했습니다.이러한 방법 중 일부는 정적이고 짧습니다.대부분의 공적인 방법이 사용되고 있기 때문에 나중에 안전하게 제거할 수 있을 것 같습니다.하지만 디버깅을 피하고 TDD 접근 방식을 사용하기 위해 테스트해보고 싶습니다.
나는 다음과 같이 생각했다.
- 답변에서 어드바이스된 메서드오브젝트가 이 때문에 과잉인 것 같습니다.
- 우선 공개적인 방법부터 시작하여 상위 레벨의 테스트에 의해 코드 커버리지가 제공되면 보호 모드로 전환하여 테스트를 제거합니다.
- 테스트 가능한 인터페이스를 사용하여 클래스를 상속하여 보호된 메서드를 공개합니다.
어느 것이 베스트 프랙티스입니까?다른 건 없나요?
JUnit은 보호된 메서드를 자동으로 공개로 변경하는 것 같습니다만, 자세히 알아보지 못했습니다.PHP에서는 리플렉션을 통한 리플렉션을 허용하지 않습니다.
PHP5(>= 5.3.2)를 PHPnit과 함께 사용하는 경우 테스트를 실행하기 전에 리플렉션(reflection)을 사용하여 비공개 및 보호된 방법을 테스트할 수 있습니다.
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(...));
...
}
티스트번은 올바른 접근을 하고 있습니다.보다 간단한 방법은 메서드를 직접 호출하여 다음과 같은 응답을 반환하는 것입니다.
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)
);
이미 알고 계신 것 같습니다만, 어쨌든 다시 말씀드리겠습니다. 보호된 방법을 테스트해야 한다면 좋지 않은 징조입니다.유닛 테스트의 목적은 클래스의 인터페이스를 테스트하는 것이며 보호된 메서드는 구현 세부 사항입니다.그렇다고 해도 말이 되는 경우도 있습니다.상속을 사용하는 경우 슈퍼클래스는 서브클래스에 인터페이스를 제공하는 것으로 표시됩니다.따라서 여기서 보호된 메서드를 테스트해야 합니다(단, 개인 메서드는 테스트하지 않습니다).이에 대한 해결책은 테스트 목적으로 하위 클래스를 만들고 이를 사용하여 메서드를 노출하는 것입니다.예:
class Foo {
protected function stuff() {
// secret stuff, you want to test
}
}
class SubFoo extends Foo {
public function exposedStuff() {
return $this->stuff();
}
}
상속을 언제든지 구성으로 대체할 수 있습니다.코드를 테스트할 때는 보통 이 패턴을 사용하는 코드를 처리하는 것이 훨씬 쉬우므로 이 옵션을 고려해 보는 것이 좋습니다.
uckelman의 답변에 정의되어 있는 getMethod()에 대해 약간의 변형을 제안합니다.
이 버전에서는 하드 코드화된 값을 삭제하고 사용을 약간 단순화함으로써 getMethod()를 변경합니다.아래 예시와 같이 PHPunitUtil 클래스 또는 PHPunit_Framework_에 추가할 것을 권장합니다.TestCase 확장 클래스(또는 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()도 작성했습니다만, 이것은 고객에게 달려 있습니다.
트롤스킨이 가까운 것 같아대신 이렇게 하겠습니다.
class ClassToTest
{
protected function testThisMethod()
{
// Implement stuff here
}
}
다음으로 다음과 같은 것을 실장합니다.
class TestClassToTest extends ClassToTest
{
public function testThisMethod()
{
return parent::testThisMethod();
}
}
그런 다음 TestClassToTest에 대해 테스트를 실행합니다.
코드를 해석함으로써 이러한 확장 클래스를 자동으로 생성할 수 있습니다.PHPUnit이 이미 그러한 메커니즘을 제공한다고 해도 이상하지 않습니다(확인하지 않았습니다).
이제 출사표를 던지겠습니다.
저는 __콜 해킹을 사용했지만 성공 정도가 엇갈렸습니다.제가 생각해낸 대안은 방문자 패턴을 사용하는 것입니다.
1: stdClass 또는 커스텀클래스 생성(유형을 강제 적용)
2: 필요한 메서드와 인수를 사용하여 프라이밍합니다.
3: SUT가 방문 클래스에서 지정된 인수를 사용하여 메서드를 실행하는 acceptVisitor 메서드를 가지고 있는지 확인합니다.
4: 테스트하고 싶은 클래스에 주입합니다.
5: SUT는 수술 결과를 방문자에게 주입합니다.
6: 테스트 조건을 방문자의 결과 속성에 적용합니다.
__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() 메서드는 클래스를 참조하지 않으므로 테스트하는 보호된 메서드를 사용하여 각 클래스에 대해 위의 내용을 복사하고 클래스 선언만 변경할 수 있습니다.이 기능을 공통의 베이스 클래스에 배치할 수 있을지도 모릅니다만, 저는 아직 시도하지 않았습니다.
테스트 케이스 자체는 테스트할 개체를 구성하는 위치에서만 다릅니다(예: 스왑).예를 들어 노출.
class ExampleTest extends PHPUnit_Framework_TestCase {
function testGetMessage() {
$fixture = new ExampleExposed();
self::assertEquals('hello', $fixture->getMessage());
}
}
PHP 5.3에서는 리플렉션을 사용하여 메서드의 접근성을 직접 변경할 수 있다고 생각합니다만, 각 메서드에 대해 개별적으로 변경해야 합니다.
"Henrik Paul"의 회피책/아이디어에 대해 다음과 같은 회피책을 제안합니다.
당신은 당신의 수업의 사적인 방법들의 이름을 알고 있습니다.예를 들어 _add(), _edit(), _delete() 등이 있습니다.
따라서 유닛 테스트의 관점에서 테스트하고 싶을 때는 private 메서드를 prefixed/fixed word(예를 들어 _addPhpunit)로 호출하여 __call() 메서드가 (메서드 _addPhpunit()이 존재하지 않으므로) __call() 메서드에 필요한 코드를 삽입하여 phunit(예를 들어 _addPhunit)을 삭제합니다.거기서부터 그 추론된 프라이빗 메서드를 호출합니다.이것은 마술의 또 다른 좋은 사용법이다.
한번 써보세요.
대안.아래 코드는 예시입니다.그 실장은 훨씬 더 넓어질 수 있다.프라이빗 메서드 테스트 및 프라이빗 속성 치환에 도움이 되는 구현입니다.
<?php
class Helper{
public static function sandbox(\Closure $call,$target,?string $slaveClass=null,...$args)
{
$slaveClass=!empty($slaveClass)?$slaveClass:(is_string($target)?$target:get_class($target));
$target=!is_string($target)?$target:null;
$call=$call->bindTo($target,$slaveClass);
return $call(...$args);
}
}
class A{
private $prop='bay';
public function get()
{
return $this->prop;
}
}
class B extends A{}
$b=new B;
$priv_prop=Helper::sandbox(function(...$args){
return $this->prop;
},$b,A::class);
var_dump($priv_prop);// bay
Helper::sandbox(function(...$args){
$this->prop=$args[0];
},$b,A::class,'hello');
var_dump($b->get());// hello
아래 코드와 같이 Closure를 사용할 수 있습니다.
<?php
class A
{
private string $value = 'Kolobol';
private string $otherPrivateValue = 'I\'m very private, like a some kind of password!';
public function setValue(string $value): void
{
$this->value = $value;
}
private function getValue(): string
{
return $this->value . ': ' . $this->getVeryPrivate();
}
private function getVeryPrivate()
{
return $this->otherPrivateValue;
}
}
$getPrivateProperty = function &(string $propName) {
return $this->$propName;
};
$getPrivateMethod = function (string $methodName) {
return Closure::fromCallable([$this, $methodName]);
};
$objA = new A;
$getPrivateProperty = Closure::bind($getPrivateProperty, $objA, $objA);
$getPrivateMethod = Closure::bind($getPrivateMethod, $objA, $objA);
$privateByLink = &$getPrivateProperty('value');
$privateMethod = $getPrivateMethod('getValue');
echo $privateByLink, PHP_EOL; // Kolobok
$objA->setValue('Zmey-Gorynich');
echo $privateByLink, PHP_EOL; // Zmey-Gorynich
$privateByLink = 'Alyonushka';
echo $privateMethod(); // Alyonushka: I'm very private, like a some kind of password!
유닛 테스트용으로 간단하게 프라이빗 메서드(스태틱 및 비스태틱)를 호출하는 클래스를 만들었습니다.
class MethodInvoker
{
public function invoke($object, string $methodName, array $args=[]) {
$privateMethod = $this->getMethod(get_class($object), $methodName);
return $privateMethod->invokeArgs($object, $args);
}
private function getMethod(string $className, string $methodName) {
$class = new \ReflectionClass($className);
$method = $class->getMethod($methodName);
$method->setAccessible(true);
return $method;
}
}
사용 예:
class TestClass {
private function privateMethod(string $txt) {
print_r('invoked privateMethod: ' . $txt);
}
}
(new MethodInvoker)->invoke(new TestClass, 'privateMethod', ['argument_1']);
언급URL : https://stackoverflow.com/questions/249664/best-practices-to-test-protected-methods-with-phpunit
'programing' 카테고리의 다른 글
다른 파일에서 변수를 가져오시겠습니까? (0) | 2022.09.09 |
---|---|
nullable 필드에 정의된 MySql 고유 제약 조건은 정확히 어떻게 작동합니까? (0) | 2022.09.09 |
동일한 워크북의 여러 워크시트에 대해 pd.read_excel()에 Panda 사용 (0) | 2022.09.09 |
Lombok은 getter와 setter를 생성하지 않습니다. (0) | 2022.09.09 |
업스트림에서 응답 헤더를 읽는 동안 업스트림에서 너무 큰 헤더가 전송되었습니다. (0) | 2022.09.09 |