使用PHPUnit测试受保护方法的最佳实践

我发现你测试私人方法吗上的讨论信息量很大。

我已经决定,在一些类中,我想要有受保护的方法,但要测试它们。 其中一些方法是静态的和简短的。因为大多数公共方法都使用了这些测试,所以我以后可能会安全地删除这些测试。但是为了从TDD方法开始并避免调试,我真的很想测试它们

我想到了以下几点:

  • 方法对象的建议在一个答案似乎是多余的。
  • 从公共方法开始,当代码覆盖由更高级别的测试提供时,将它们变为受保护的,并删除测试。
  • 继承一个具有可测试接口的类,该接口使受保护的方法公开

哪种是最佳实践?还有别的事吗?

看起来,JUnit会自动将受保护的方法更改为公共方法,但我并没有深入了解它。PHP不允许通过反射这样做。

162966 次浏览

你们似乎已经意识到了,但我还是要重申一下;如果您需要测试受保护的方法,这是一个不好的迹象。单元测试的目的是测试类的接口,而受保护的方法是实现细节。也就是说,在某些情况下,这是有道理的。如果使用继承,可以看到父类为子类提供接口。所以在这里,你必须测试受保护的方法(但绝不是私人方法)。解决这个问题的方法是创建一个用于测试的子类,并使用它来公开方法。如:

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


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

注意,您总是可以用组合替换继承。在测试代码时,处理使用这种模式的代码通常要容易得多,因此您可能需要考虑该选项。

我想troelskn就在附近。我会这样做:

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

然后,实现如下内容:

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

然后根据TestClassToTest运行测试。

应该可以通过解析代码自动生成这样的扩展类。如果PHPUnit已经提供了这样的机制,我不会感到惊讶(尽管我还没有检查)。

我建议以下“亨里克·保罗”的解决方案/想法的解决方案:)

您知道类的私有方法的名称。例如,它们像_add(), _edit(), _delete()等。

因此,当你想从单元测试的角度测试它时,只需通过添加前缀和/或后缀一些常见的单词(例如_addPhpunit)来调用私有方法,这样当所有者类的__call()方法被调用时(因为方法_addPhpunit()不存在),你只需在__call()方法中放入必要的代码来删除带前缀/后缀的单词/s (Phpunit),然后从那里调用推断出的私有方法。这是另一个很好的使用魔术的方法。

试试吧。

你确实可以以通用的方式使用__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。

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

我相信PHP 5.3允许您使用反射来直接更改方法的可访问性,但我假设您必须对每个方法单独这样做。

如果你在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(...));
...
}

我在这里郑重声明:

我已经使用了__call hack,成功程度不一。 我提出的替代方案是使用访问者模式:

1:生成一个stdClass或自定义类(以加强类型)

2:用所需的方法和参数来启动它

3:确保你的SUT有一个acceptVisitor方法,它将使用访问类中指定的参数执行方法

4:将其注入到您希望测试的类中

5: SUT将操作结果注入访问者

6:将测试条件应用到访问者的结果属性

我想建议对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()来显式显示期望的内容,但这取决于您。

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)
);
< p >选择。下面的代码是作为示例提供的。 它的实施可以更广泛。 它的实现将帮助你测试私有方法和替换私有属性
    <?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

你可以在下面的代码中使用关闭

<?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']);