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

时间:2008-10-30 09:32:42

标签: php unit-testing phpunit

我在Do you test private method提供了有关信息的讨论。

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

我想到了以下内容:

  • Method Object正如an answer中所提出的建议似乎有点矫枉过正。
  • 从公共方法开始,当更高级别的测试提供代码覆盖时,将其保护并删除测试。
  • 继承具有可测试接口的类,使受保护的方法公开

哪种是最佳做法?还有别的吗?

看来,JUnit会自动将受保护的方法更改为公开,但我没有深入了解它。 PHP不允许通过reflection

8 个答案:

答案 0 :(得分:388)

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

答案 1 :(得分:45)

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

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

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

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

答案 2 :(得分:33)

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)
             );

答案 3 :(得分:19)

我想对uckelman's answer中定义的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()来明确所期望的内容,但这取决于你。

干杯!

答案 4 :(得分:9)

我认为troelskn很接近。我会这样做:

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

然后,实现类似的东西:

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

然后针对TestClassToTest运行测试。

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

答案 5 :(得分:4)

您确实可以以通用方式使用__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 for Example中进行交换。

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

我相信PHP 5.3允许您使用反射直接更改方法的可访问性,但我认为您必须单独为每个方法执行此操作。

答案 6 :(得分:4)

我要把帽子扔进戒指:

我使用了__call hack,取得了不同程度的成功。 我提出的替代方案是使用访客模式:

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

2:用所需的方法和参数填充

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

4:将它注入你想要测试的班级

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

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

答案 7 :(得分:2)

我建议遵循“Henrik Paul”的解决方法/想法的解决方法:)

您知道班级私人方法的名称。例如,它们就像_​​add(),_ edit(),_ delete()等。

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

尝试一下。