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


286

我发现有关“您测试私有方法的信息性” 的讨论。

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

我想到了以下几点:

  • 答案中建议的“ 方法对象 ”对此似乎是过大的。
  • 从公共方法开始,当更高级别的测试提供了代码覆盖范围时,请将其置于保护状态并删除测试。
  • 继承具有可测试接口的类,使受保护的方法公开

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

看来,JUnit会自动将受保护的方法更改为公共方法,但我对此没有更深入的了解。PHP不允许通过反射进行此操作。


两个问题:1.为什么要打扰您的班级未公开的测试功能?2.如果您应该测试它,为什么它是私有的?
2011年

2
也许他想测试私有财产是否设置正确,并且仅使用setter函数进行测试的唯一方法是公开私有财产并检查数据
AntonioCS 2012年

4
因此,这是讨论式的,因此没有建设性。再次:)
mlvljr 2012年

72
您可以按照网站的规则来称呼它,但是仅仅称其为“非建设性的”是……这很侮辱。
Andy V

1
@Visser,这是对自己的侮辱;)
Pacerier,2015年

Answers:


415

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

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
引用塞巴斯蒂安博客的链接:“所以:仅仅因为可以测试受保护的和私有的属性和方法并不意味着这是一件“好事”。” -记住这一点
edorian 2011年

10
我会争辩。如果您不需要使用受保护的方法或私有方法,请不要对其进行测试。
uckelman 2011年

10
只是为了澄清,您不需要使用PHPUnit即可工作。它也可以与SimpleTest或其他工具一起使用。答案与PHPUnit无关。
伊恩·邓恩

84
您不应直接测试受保护/私有成员。它们属于该类的内部实现,不应与测试耦合。这使得重构变得不可能,并且最终您不测试需要测试的内容。您需要使用公共方法间接测试它们。如果发现这很困难,则几乎可以确定该类的组成存在问题,您需要将其分成较小的类。请记住,您的课程应该是测试的黑匣子-您投入一些东西,然后得到一些回报,仅此而已!
gphilip

24
@gphilip对我来说,protected方法也是公共api的一部分,因为任何第三方类都可以在没有任何魔术的情况下扩展和使用它。因此,我认为只有private方法属于无法直接测试的方法类别。protectedpublic应直接进行测试。
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()。看到我的回应。看来我今天看书太快了。
迈克尔·约翰逊

你是对的; 将受保护的方法更改为公共方法是有效的。
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
这是一个很好的例子,谢谢。该方法应该是公开的,而不是受保护的,不是吗?
2012年

好点子。我实际上是在扩展测试类的基础类中使用此方法的,在这种情况下,这是有道理的。但是,该类的名称在这里是错误的。
robert.egginton 2012年

我基于teastburn xD编写了完全相同的代码
Nebulosar

23

我想对uckelman的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()来明确期望的内容,但这取决于您。

干杯!


+1用于使用反射类API。
Bill Ortell 2013年

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

仅供参考,此功能仅适用于受保护的方法,不适用于私有方法
Sliq,

5

我要在这里戴上帽子:

我曾经使用__call hack取得了不同程度的成功。我想到的替代方法是使用Visitor模式:

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

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

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

4:将其注入您要测试的班级

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

6:将测试条件应用于“访客”的结果属性


1
+1是一个有趣的解决方案
jsh 2014年

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()实现效果很好!我尝试投票,但直到测试该方法后我才取消投票,但由于SO的时间限制,我现在无法投票。
亚当·佛朗哥

call_user_method_array()自PHP 4.1.0起不推荐使用该函数... call_user_func_array(array($this, $method), $args)改为使用。请注意,如果您使用的是PHP 5.3.2+,则可以使用Reflection 获得对受保护/私有方法和属性的访问
nuqqsa 2011年

@nuqqsa-谢谢,我更新了答案。此后,我编写了一个通用Accessible包,该包使用反射来允许测试访问私有/受保护的属性以及类和对象的方法。
David Harkness,

在PHP 5.2.7上,此代码不适用于我-__call方法不会为基类定义的方法调用。我找不到它的记录,但我猜测此行为已在PHP 5.3中更改(我确认它可以正常工作)。
罗素·戴维斯

@Russell- __call()仅在调用者无权访问该方法时被调用。由于该类及其子类可以访问受保护的方法,因此对它们的调用不会通过__call()。您可以在新问题中发布在5.2.7中不起作用的代码吗?我在5.2中使用了以上内容,而仅在5.3.2中使用了反射。
David Harkness

2

我建议按照以下“ Henrik Paul”的解决方法/想法进行解决:)

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

因此,当您要从单元测试的角度对其进行测试时,只需通过在一些常见的前缀和/或后缀上调用私有方法即可词(例如_addPhpunit)后以便在调用__call()方法时(因为方法_addPhpunit()不会)所有者类的存在),您只需将必要的代码放在__call()方法中,以删除带有前缀/后缀的单词(Phpunit),然后从那里调用推导的私有方法。这是魔术方法的另一种很好的用法。

试试看。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.