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

时间:2022-10-16 00:14:18

I found the discussion on Do you test private method informative.

我发现关于你是否测试私有方法的讨论非常有用。

I have decided, that in some classes, I want to have protected methods, but test them. Some of these methods are static and short. Because most of the public methods make use of them, I will probably be able to safely remove the tests later. But for starting with a TDD approach and avoid debugging, I really want to test them.

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

I thought of the following:

我想到了以下几点:

  • Method Object as adviced in an answer seems to be overkill for this.
  • 方法对象在回答中建议这样做似乎有点过头了。
  • Start with public methods and when code coverage is given by higher level tests, turn them protected and remove the tests.
  • 从公共方法开始,当更高级别测试给出代码覆盖率时,将它们设置为protected并删除测试。
  • Inherit a class with a testable interface making protected methods public
  • 继承一个具有可测试接口的类,使受保护的方法公开。

Which is best practice? Is there anything else?

最佳实践是什么?还有什么?

It seems, that JUnit automatically changes protected methods to be public, but I did not have a deeper look at it. PHP does not allow this via reflection.

看起来,JUnit自动地改变了受保护的方法以公开,但是我没有深入的看它。PHP不允许通过反射实现这一点。

8 个解决方案

#1


356  

If you're using PHP5 (>= 5.3.2) with PHPUnit, you can test your private and protected methods by using reflection to set them to be public prior to running your tests:

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

#2


39  

You seem to be aware already, but I'll just restate it anyway; It's a bad sign, if you need to test protected methods. The aim of a unit test, is to test the interface of a class, and protected methods are implementation details. That said, there are cases where it makes sense. If you use inheritance, you can see a superclass as providing an interface for the subclass. So here, you would have to test the protected method (But never a private one). The solution to this, is to create a subclass for testing purpose, and use this to expose the methods. Eg.:

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

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

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

Note that you can always replace inheritance with composition. When testing code, it's usually a lot easier to deal with code that uses this pattern, so you may want to consider that option.

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

#3


27  

teastburn has the right approach. Even simpler is to call the method directly and return the answer:

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

You can call this simply in your tests by:

您可以在测试中简单地将其命名为:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );

#4


17  

I'd like to propose a slight variation to getMethod() defined in uckelman's answer.

我想对uckelman的答案中定义的getMethod()进行一点修改。

This version changes getMethod() by removing hard-coded values and simplifying usage a little. I recommend adding it to your PHPUnitUtil class as in the example below or to your PHPUnit_Framework_TestCase-extending class (or, I suppose, globally to your PHPUnitUtil file).

这个版本通过删除硬编码的值和简化使用来改变getMethod()。我建议将它添加到以下示例中的PHPUnitUtil类或phpunit_framework_testcase扩展类(或者,我认为,全局地添加到PHPUnitUtil文件)中。

Since MyClass is being instantiated anyways and ReflectionClass can take a string or an object...

由于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
}

I also created an alias function getProtectedMethod() to be explicit what is expected, but that one's up to you.

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

Cheers!

干杯!

#5


8  

I think troelskn is close. I would do this instead:

我觉得troelskn很接近了。我会这样做:

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

Then, implement something like this:

然后,执行如下操作:

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

You then run your tests against TestClassToTest.

然后对TestClassToTest运行测试。

It should be possible to automatically generate such extension classes by parsing the code. I wouldn't be surprised if PHPUnit already offers such a mechanism (though I haven't checked).

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

#6


3  

I'm going to throw my hat into the ring here:

我要把我的帽子扔进戒指里:

I've used the __call hack with mixed degrees of success. The alternative I came up with was to use the Visitor pattern:

我用__call hack取得了不同程度的成功。我提出的另一种选择是使用访问者模式:

1: generate a stdClass or custom class (to enforce type)

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

2: prime that with the required method and arguments

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

3: ensure that your SUT has an acceptVisitor method which will execute the method with the arguments specified in the visiting class

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

4: inject it into the class you wish to test

4:将它注入要测试的类中

5: SUT injects the result of operation into the visitor

5: SUT将手术结果注入来访者体内

6: apply your test conditions to the Visitor's result attribute

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

#7


3  

You can indeed use __call() in a generic fashion to access protected methods. To be able to test this class

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

class Example {
    protected function getMessage() {
        return 'hello';
    }
}

you create a subclass in ExampleTest.php:

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

Note that the __call() method does not reference the class in any way so you can copy the above for each class with protected methods you want to test and just change the class declaration. You may be able to place this function in a common base class, but I haven't tried it.

注意,__call()方法不以任何方式引用类,因此您可以使用您想要测试的受保护方法复制上述的类,并更改类声明。您可以将这个函数放在一个公共基类中,但是我还没有尝试过。

Now the test case itself only differs in where you construct the object to be tested, swapping in ExampleExposed for Example.

现在,测试用例本身只在构造要测试的对象的地方有所不同,例如在示例中进行交换。

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

I believe PHP 5.3 allows you to use reflection to change the accessibility of methods directly, but I assume you'd have to do so for each method individually.

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

#8


2  

I suggest following workaround for "Henrik Paul"'s workaround/idea :)

我建议遵循“亨利克·保罗”的解决方案/想法:)

You know names of private methods of your class. For example they are like _add(), _edit(), _delete() etc.

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

Hence when you want to test it from aspect of unit-testing, just call private methods by prefixing and/or suffixing some common word (for example _addPhpunit) so that when __call() method is called (since method _addPhpunit() doesn't exist) of owner class, you just put necessary code in __call() method to remove prefixed/suffixed word/s (Phpunit) and then to call that deduced private method from there. This is another good use of magic methods.

因此当你想测试它从单元测试方面,就叫私有方法的前缀和/或向一些常见词(例如_addPhpunit)所以当__call()方法(因为方法_addPhpunit()不存在)的所有者阶级,你把必要的代码在__call()方法删除前缀/后缀词/ s(Phpunit)),然后调用,推导出私有方法。这是魔法方法的另一个很好的应用。

Try it out.

试一下。

#1


356  

If you're using PHP5 (>= 5.3.2) with PHPUnit, you can test your private and protected methods by using reflection to set them to be public prior to running your tests:

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

#2


39  

You seem to be aware already, but I'll just restate it anyway; It's a bad sign, if you need to test protected methods. The aim of a unit test, is to test the interface of a class, and protected methods are implementation details. That said, there are cases where it makes sense. If you use inheritance, you can see a superclass as providing an interface for the subclass. So here, you would have to test the protected method (But never a private one). The solution to this, is to create a subclass for testing purpose, and use this to expose the methods. Eg.:

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

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

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

Note that you can always replace inheritance with composition. When testing code, it's usually a lot easier to deal with code that uses this pattern, so you may want to consider that option.

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

#3


27  

teastburn has the right approach. Even simpler is to call the method directly and return the answer:

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

You can call this simply in your tests by:

您可以在测试中简单地将其命名为:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );

#4


17  

I'd like to propose a slight variation to getMethod() defined in uckelman's answer.

我想对uckelman的答案中定义的getMethod()进行一点修改。

This version changes getMethod() by removing hard-coded values and simplifying usage a little. I recommend adding it to your PHPUnitUtil class as in the example below or to your PHPUnit_Framework_TestCase-extending class (or, I suppose, globally to your PHPUnitUtil file).

这个版本通过删除硬编码的值和简化使用来改变getMethod()。我建议将它添加到以下示例中的PHPUnitUtil类或phpunit_framework_testcase扩展类(或者,我认为,全局地添加到PHPUnitUtil文件)中。

Since MyClass is being instantiated anyways and ReflectionClass can take a string or an object...

由于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
}

I also created an alias function getProtectedMethod() to be explicit what is expected, but that one's up to you.

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

Cheers!

干杯!

#5


8  

I think troelskn is close. I would do this instead:

我觉得troelskn很接近了。我会这样做:

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

Then, implement something like this:

然后,执行如下操作:

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

You then run your tests against TestClassToTest.

然后对TestClassToTest运行测试。

It should be possible to automatically generate such extension classes by parsing the code. I wouldn't be surprised if PHPUnit already offers such a mechanism (though I haven't checked).

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

#6


3  

I'm going to throw my hat into the ring here:

我要把我的帽子扔进戒指里:

I've used the __call hack with mixed degrees of success. The alternative I came up with was to use the Visitor pattern:

我用__call hack取得了不同程度的成功。我提出的另一种选择是使用访问者模式:

1: generate a stdClass or custom class (to enforce type)

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

2: prime that with the required method and arguments

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

3: ensure that your SUT has an acceptVisitor method which will execute the method with the arguments specified in the visiting class

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

4: inject it into the class you wish to test

4:将它注入要测试的类中

5: SUT injects the result of operation into the visitor

5: SUT将手术结果注入来访者体内

6: apply your test conditions to the Visitor's result attribute

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

#7


3  

You can indeed use __call() in a generic fashion to access protected methods. To be able to test this class

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

class Example {
    protected function getMessage() {
        return 'hello';
    }
}

you create a subclass in ExampleTest.php:

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

Note that the __call() method does not reference the class in any way so you can copy the above for each class with protected methods you want to test and just change the class declaration. You may be able to place this function in a common base class, but I haven't tried it.

注意,__call()方法不以任何方式引用类,因此您可以使用您想要测试的受保护方法复制上述的类,并更改类声明。您可以将这个函数放在一个公共基类中,但是我还没有尝试过。

Now the test case itself only differs in where you construct the object to be tested, swapping in ExampleExposed for Example.

现在,测试用例本身只在构造要测试的对象的地方有所不同,例如在示例中进行交换。

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

I believe PHP 5.3 allows you to use reflection to change the accessibility of methods directly, but I assume you'd have to do so for each method individually.

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

#8


2  

I suggest following workaround for "Henrik Paul"'s workaround/idea :)

我建议遵循“亨利克·保罗”的解决方案/想法:)

You know names of private methods of your class. For example they are like _add(), _edit(), _delete() etc.

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

Hence when you want to test it from aspect of unit-testing, just call private methods by prefixing and/or suffixing some common word (for example _addPhpunit) so that when __call() method is called (since method _addPhpunit() doesn't exist) of owner class, you just put necessary code in __call() method to remove prefixed/suffixed word/s (Phpunit) and then to call that deduced private method from there. This is another good use of magic methods.

因此当你想测试它从单元测试方面,就叫私有方法的前缀和/或向一些常见词(例如_addPhpunit)所以当__call()方法(因为方法_addPhpunit()不存在)的所有者阶级,你把必要的代码在__call()方法删除前缀/后缀词/ s(Phpunit)),然后调用,推导出私有方法。这是魔法方法的另一个很好的应用。

Try it out.

试一下。