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

GrG*_*rGr 275 php phpunit unit-testing

我发现讨论你是否测试私人方法的信息.

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

我想到了以下几点:

  • 答案中建议的方法对象似乎有点矫枉过正.
  • 从公共方法开始,当更高级别的测试给出代码覆盖时,将它们保护并删除测试.
  • 继承具有可测试接口的类,使受保护的方法公开

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

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

uck*_*man 403

如果您在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(...));
  ...
}
Run Code Online (Sandbox Code Playgroud)

  • 您不应直接测试受保护/私有成员.它们属于类的内部实现,不应该与测试结合.这使得重构变得不可能,最终你不会测试需要测试的内容.您需要使用公共方法间接测试它们.如果您发现这很困难,几乎可以肯定该类的组成存在问题,您需要将它分成较小的类.请记住,你的课程应该是一个黑盒子供你考试 - 你扔东西然后你会得到回报,这就是全部! (79认同)
  • 引用sebastians博客的链接:*"所以:仅仅因为对受保护和私有属性和方法的测试是可能的并不意味着这是一个"好事"."* - 只是为了记住这一点 (26认同)
  • @gphilip对我来说,`protected`方法也是公共api的一部分,因为**任何第三方类都可以扩展它并使用它**没有任何魔法.所以我认为只有"私有"方法属于不能直接测试的方法类别.应该直接测试`protected`和`public`. (16认同)
  • 我会争辩的.如果您不需要使用受保护或私有方法,请不要测试它们. (9认同)
  • 只是为了澄清,您不需要使用PHPUnit来实现这一点.它也适用于SimpleTest或其他任何东西.没有关于依赖于PHPUnit的答案. (9认同)
  • 我没有听到人们说不要仅仅因为测试私有/受保护的函数。如果您的公共方法依赖于两个您不想公开的私有方法,那么您不会测试这些私有方法吗?仅仅为了它而公开所有内容就像 chmoding 服务器 777 中的每个文件一样,这样您就不会收到权限错误。 (5认同)
  • 很好的解决方案.可能想添加虽然这只是php> = 5.3.2;) (4认同)
  • @FilipHalaxa这是正确的,这正是受保护成员存在问题的原因,至少可以这么说.大量使用它们可能表明设计不佳.对于可测试性,代码重用和易于理解,更喜欢组合而不是继承,并尽可能避免脆弱的受保护成员. (4认同)
  • 是的,我认为gphilip评论是最好的答案. (3认同)
  • 在我看来,单元测试应该意识到像我这样的一些/很多开发人员都会选择测试驱动设计.TDD是全面的,受保护的方法应该被涵盖.此外,我需要设计简单,受保护的方法来执行复杂的查询(即使在使用CRUD类之后)并且想要与调用方法分开测试它们. (3认同)
  • @gphilip与此逻辑有关的另一个问题是,单元测试不仅有助于表明“某些东西已损坏”,而且还有助于识别“已损坏的东西”。想象一个具有一个公共方法和十个受保护方法的类。对受保护的关键方法进行测试可能会使快速识别出损坏的内容变得容易得多。(尽管已达成共识;这种情况可能表明设计不佳) (2认同)
  • 嗯,当开始处理一个“继承的”代码库时,它具有一个*咳咳*有趣的结构,涉及到各处的受保护和静态方法,这是很有价值的。因此,清理混乱的第一步是添加单元测试,并且能够在较小的单元上执行此操作,而不是在唯一调用 20 个受保护方法的公共方法(在其中调用另外 5-10 个受保护方法) !)更可行。 (2认同)

tro*_*skn 47

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

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

class SubFoo extends Foo {
  public function exposedStuff() {
    return $this->stuff();
  }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我不同意这是一个不好的迹象.让我们在TDD和单元测试之间做出改变.单元测试应该测试私有方法imo,因为这些是单元并且将以与单元测试公共方法受益于单元测试相同的方式受益. (32认同)
  • 受保护的方法*是*类的接口的一部分,它们不仅仅是实现细节.受保护成员的全部意义在于,子类(用户自己)可以在类别exstions中使用这些受保护的方法.那些显然需要进行测试. (32认同)
  • 你可以直接将stuff()实现为public并返回parent :: stuff().看看我的回复.看来我今天读的东西太快了. (2认同)
  • “单元测试的目的是测试类的接口......”您从哪里得到这个结论?在我看来,单元测试是关于测试代码的小单元。可能是一个函数。无论是“私有”、“受保护”还是“公共”,这实际上都是实现细节。无论您的单元是否是“公共”接口的一部分,如果经过测试,它都会受到保护,免受可能破坏其逻辑的不需要的更改。您是否曾经需要修复“私有”方法中的错误?如果是这样,单元测试是否有助于避免该错误?我不敢相信人们正在争论这个。 (2认同)

小智 37

特斯特本有正确的方法.更简单的是直接调用方法并返回答案:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以通过以下方式在测试中简单地调用它:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );
Run Code Online (Sandbox Code Playgroud)


tea*_*urn 20

我想对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
}
Run Code Online (Sandbox Code Playgroud)

我还创建了一个别名函数getProtectedMethod()来明确预期的内容,但这取决于你.

干杯!


Mic*_*son 10

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

class ClassToTest
{
   protected function testThisMethod()
   {
     // Implement stuff here
   }
}
Run Code Online (Sandbox Code Playgroud)

然后,实现这样的事情:

class TestClassToTest extends ClassToTest
{
  public function testThisMethod()
  {
    return parent::testThisMethod();
  }
}
Run Code Online (Sandbox Code Playgroud)

然后,您将针对TestClassToTest运行测试.

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

  • 是的,那正是我的第三个选择。我很确定,PHPUnit不提供这种机制。 (2认同)
  • 仅供参考,这只适用于**受保护的**方法,不适用于私有方法 (2认同)

Dav*_*ess 5

您确实可以以通用方式使用__call()来访问受保护的方法。为了能够测试这堂课

class Example {
    protected function getMessage() {
        return 'hello';
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以在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);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,__call()方法不会以任何方式引用该类,因此您可以使用要测试的受保护方法为每个类复制以上内容,而只需更改类声明即可。您可能可以将此函数放在一个通用的基类中,但是我还没有尝试过。

现在,测试用例本身仅在构建要测试的对象方面有所不同,将ExampleExposed替换为Example。

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

我相信PHP 5.3允许您使用反射直接更改方法的可访问性,但是我认为您必须分别为每个方法进行更改。


sun*_*ung 5

我将把帽子戴在这里:

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

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

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

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

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

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

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