关于php:使用PHPUnit测试受保护方法的最佳实践

Best practices to test protected methods with PHPUnit

我发现关于测试私有方法的讨论很有启发性。

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

我想到了以下几点:

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

哪一个是最佳实践?还有别的吗?

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


如果将php5(>=5.3.2)与phpunit一起使用,则可以在运行测试之前使用反射将私有方法和受保护方法设置为公共方法来测试它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
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(...));
  ...
}


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

1
2
3
4
5
6
7
8
9
10
11
class Foo {
  protected function stuff() {
    // secret stuff, you want to test
  }
}

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

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


茶壶有正确的方法。更简单的方法是直接调用方法并返回答案:

1
2
3
4
5
6
7
8
9
10
class PHPUnitUtil
{
  public static function callMethod($obj, $name, array $args) {
        $class = new
eflectionClass($obj);
        $method = $class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($obj, $args);
    }
}

在您的测试中,您可以通过以下方式简单地称之为:

1
2
3
4
5
$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod',
                array($arg1, $arg2)
             );


我想对在Uckelman的答案中定义的getMethod()提出一个微小的变化。

此版本通过删除硬编码值并稍微简化用法来更改getmethod()。我建议将它添加到phpunitil类中,如下面的示例所示,或者添加到phpunit_framework_testcase-extending类(或者,我认为,全局添加到phpunitil文件中)。

因为MyClass是以任何方式实例化的,ReflectionClass可以采用字符串或对象…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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()来显式地表示期望的内容,但这取决于您自己。

干杯!


我觉得特罗尔斯克很接近。我会这样做:

1
2
3
4
5
6
7
class ClassToTest
{
   protected testThisMethod()
   {
     // Implement stuff here
   }
}

然后,执行如下操作:

1
2
3
4
5
6
7
class TestClassToTest extends ClassToTest
{
  public testThisMethod()
  {
    return parent::testThisMethod();
  }
}

然后根据testclasstotest运行测试。

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


我要把帽子扔到戒指里:

我使用了"黑客"这个词,成功程度参差不齐。我提出的另一种选择是使用访客模式:

1:生成stdclass或自定义类(强制类型)

2:用所需的方法和参数为其打底

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

4:把它注入你想要测试的类中。

5:SUT向来访者注入手术结果

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


您确实可以用一般的方式使用uu call()来访问受保护的方法。能够测试这个类

1
2
3
4
5
class Example {
    protected function getMessage() {
        return 'hello';
    }
}

在exampletest.php中创建子类:

1
2
3
4
5
6
7
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);
    }
}

注意,uu call()方法不以任何方式引用类,因此您可以使用要测试的受保护方法为每个类复制上面的内容,只需更改类声明即可。您可以将这个函数放在一个公共的基类中,但我没有尝试过。

现在,测试用例本身只在构建要测试的对象的位置上有所不同,例如交换示例exposed。

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

我相信PHP5.3允许您使用反射来直接更改方法的可访问性,但是我假设您必须为每个方法分别这样做。


我建议为"Henrik Paul"的解决方案/想法提供以下解决方案:

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

因此,当您想从单元测试方面测试它时,只需通过前缀和/或后缀一些常用词(例如Addphpunit)来调用私有方法,这样当调用所有者类的Call()方法(因为方法Addphpunit()不存在)时,只需在Call()方法中放置必要的代码,以删除前缀/后缀词/s(phpunit)。然后从那里调用推导出的私有方法。这是另一种很好的魔法方法。

试试看。