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

Posted

技术标签:

【中文标题】使用 PHPUnit 测试受保护方法的最佳实践【英文标题】:Best practices to test protected methods with PHPUnit 【发布时间】:2021-12-24 01:51:17 【问题描述】:

我发现Do you test private method 上的讨论内容丰富。

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

我想到了以下几点:

Method Object 在an answer 中的建议似乎是矫枉过正。 从公共方法开始,当代码覆盖率由更高级别的测试提供时,将它们设为保护并删除测试。 继承具有可测试接口的类,使受保护的方法公开

最佳做法是什么?还有什么吗?

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

【问题讨论】:

也许他想测试一个私有属性是否设置正确,而仅使用 setter 函数进行测试的唯一方法是公开私有属性并检查数据 所以这是讨论式的,因此没有建设性。再次:) 您可以称其为违反网站规则,但仅称其为“不具建设性”是......这是侮辱。 @Visser,这是在侮辱自己;) 完全有可能他可能不想测试公共类接口本身,而是它呈现给子类的接口(可以访问受保护的方法)跨度> 【参考方案1】:

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

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


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

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

【讨论】:

你可以直接将 stuff() 实现为 public 并返回 parent::stuff()。看我的回复。看来我今天读的太快了。 你是对的;将受保护的方法更改为公共方法是有效的。 我不同意这是一个不好的迹象。让我们区分一下 TDD 和单元测试。单元测试应该测试私有方法 imo,因为这些是单元,并且会像单元测试公共方法从单元测试中受益一样受益。 受保护的方法类接口的一部分,它们不仅仅是实现细节。受保护成员的全部意义在于子类(用户自己)可以在类扩展中使用那些受保护的方法。这些显然需要测试。 “单元测试的目的,是测试一个类的接口……” 你从哪里得出这个结论的?在我看来,单元测试是关于测试代码的小单元。可能是一个函数。无论是privateprotected 还是public,这实际上都是一个实现细节。无论您的单元是否是public 接口的一部分,如果它经过测试,它就会受到保护,不会发生可能破坏其逻辑的意外更改。您是否曾经不得不修复 private 方法中的错误?如果是这样,单元测试是否有助于避免该错误?我不敢相信人们正在争论这个。【参考方案2】:

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

class ClassToTest

   protected function testThisMethod()
   
     // Implement stuff here
   

然后,实现如下:

class TestClassToTest extends ClassToTest

  public function testThisMethod()
  
    return parent::testThisMethod();
  

然后您针对 TestClassToTest 运行测试。

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

【讨论】:

嘿...看来我是在说,使用你的第三个选项:) 是的,这正是我的第三个选择。我很确定,PHPUnit 不提供这样的机制。 这不行,你不能用同名的公共函数覆盖受保护的函数。 我可能错了,但我认为这种方法行不通。 PHPUnit(就我曾经使用过的而言)要求您的测试类扩展另一个提供实际测试功能的类。除非有办法解决,否则我不确定我能否看到如何使用这个答案。 phpunit.de/manual/current/en/… 仅供参考,这仅适用于受保护的方法,不适用于私有方法【参考方案3】:

我建议对“Henrik Paul”的解决方法/想法采取以下解决方法:)

你知道你的类的私有方法的名字。例如,它们就像 _add()、_edit()、_delete() 等。

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

试试看。

【讨论】:

使得查找私有方法调用的引用变得更加困难,不是吗?【参考方案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 中交换为 Example。

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

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

【讨论】:

__call() 实现效果很好!我试图投票,但直到我测试了这个方法之后我才取消投票,现在由于 SO 的时间限制,我不允许投票。 自 PHP 4.1.0 起,call_user_method_array() 函数已被弃用 ... 请改用 call_user_func_array(array($this, $method), $args)。请注意,如果您使用的是 PHP 5.3.2+,您可以使用 Reflection to gain access to protected/private methods and attributes @nuqqsa - 谢谢,我更新了我的答案。从那以后,我编写了一个通用的Accessible 包,它使用反射来允许测试访问类和对象的私有/受保护属性和方法。 这段代码在 PHP 5.2.7 上对我不起作用—— __call 方法不会被基类定义的方法调用。我找不到它的文档,但我猜这种行为在 PHP 5.3 中发生了变化(我已经确认它有效)。 @Russell - __call() 只有在调用者无权访问该方法时才会被调用。由于类及其子类可以访问受保护的方法,因此对它们的调用不会通过__call()。您可以在新问题中发布在 5.2.7 中不起作用的代码吗?我在 5.2 中使用了上述内容,仅在 5.3.2 中使用反射。【参考方案5】:

如果您将 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(...));
  ...

【讨论】:

引用 sebastians 博客的链接:“所以:仅仅因为受保护和私有属性和方法的测试是可能的,并不意味着这是一件“好事”。” i> - 记住这一点 我会反对这一点。如果您不需要受保护或私有的方法来工作,请不要测试它们。 只是为了澄清,您不需要使用 PHPUnit 来工作。它也适用于 SimpleTest 或其他任何东西。答案与 PHPUnit 无关。 您不应直接测试受保护/私有成员。它们属于类的内部实现,不应与测试耦合。这使得重构变得不可能,最终您不会测试需要测试的内容。您需要使用公共方法间接测试它们。如果你觉得这很困难,几乎可以肯定班级的组成有问题,你需要将它分成更小的班级。请记住,您的课程应该是您的测试的黑匣子——您投入一些东西,然后又得到一些东西,仅此而已! @gphilip 对我来说,protected 方法也是公共 api 的一部分,因为任何第三方类都可以扩展并使用它而无需任何魔法。所以我认为只有private方法属于不直接测试的方法类别。 protectedpublic 应该直接测试。【参考方案6】:

我要把我的帽子扔进擂台上:

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

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

2:使用所需的方法和参数进行初始化

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

4:将其注入您要测试的类中

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

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

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

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

【讨论】:

这是一个很好的例子,谢谢。该方法应该是公开的而不是受保护的,不是吗? 好点。我实际上在我扩展我的测试类的基类中使用了这个方法,在这种情况下这是有道理的。不过这里的类名是错误的。 我根据 teastburn xD 制作了完全相同的代码【参考方案9】:

替代方案。以下代码作为示例提供。 它的实施可以更广泛。 它的实现将帮助您测试私有方法并替换私有属性。

    <?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

【讨论】:

【参考方案10】:

您可以在下面的代码中使用Closure

<?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!

【讨论】:

以上是关于使用 PHPUnit 测试受保护方法的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

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

PHPUnit测试使用pdo的受保护静态方法

使用 PHPUnit 对具有多种用户类型的网站进行单元测试的最佳方法

PHPUnit中的测试方法应该如何命名

使用 Keycloak 时模拟“生成 API 令牌”的最佳实践

最佳实践:如何保护来自移动应用程序的 Http 请求(例如登录)