我发现关于你是否测试私有方法的讨论内容丰富。
我已经决定,在某些类中,我希望有受保护的方法,但要测试它们。其中一些方法是静态的和简短的。因为大多数公共方法都使用它们,所以我以后可能可以安全地删除测试。但是为了从 TDD 方法开始并避免调试,我真的很想测试它们。
我想到了以下几点:
哪个是最佳实践?还有别的事吗?
看起来,JUnit 自动将受保护的方法更改为公共的,但我没有深入研究它。PHP 不允许通过反射进行此操作。
如果您将 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(...));
...
}
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)
);
您似乎已经意识到了,但无论如何我还是要重申一下;如果您需要测试受保护的方法,这是一个不好的迹象。单元测试的目的是测试一个类的接口,保护方法是实现细节。也就是说,在某些情况下它是有意义的。如果使用继承,则可以将超类视为为子类提供接口。所以在这里,您必须测试受保护的方法(但绝不是私有方法)。对此的解决方案是创建一个用于测试目的的子类,并使用它来公开方法。例如。:
class Foo {
protected function stuff() {
// secret stuff, you want to test
}
}
class SubFoo extends Foo {
public function exposedStuff() {
return $this->stuff();
}
}
请注意,您始终可以用组合替换继承。在测试代码时,处理使用这种模式的代码通常要容易得多,因此您可能需要考虑该选项。
我想对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
}
我还创建了一个别名函数 getProtectedMethod() 来明确预期的内容,但这取决于你。
我认为troelskn很接近。我会这样做:
class ClassToTest
{
protected function testThisMethod()
{
// Implement stuff here
}
}
然后,实现这样的事情:
class TestClassToTest extends ClassToTest
{
public function testThisMethod()
{
return parent::testThisMethod();
}
}
然后,您针对 TestClassToTest 运行测试。
应该可以通过解析代码自动生成这样的扩展类。如果 PHPUnit 已经提供了这样的机制,我不会感到惊讶(尽管我没有检查过)。
您确实可以以通用方式使用 __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 hack 取得了不同程度的成功。我想出的替代方案是使用访问者模式:
1:生成stdClass或自定义类(强制类型)
2:使用所需的方法和参数进行初始化
3:确保您的 SUT 有一个 acceptVisitor 方法,该方法将使用访问类中指定的参数执行该方法
4:将其注入您要测试的类中
5:SUT将操作结果注入访问者
6:将您的测试条件应用于访问者的结果属性
我建议遵循“Henrik Paul”的解决方法/想法的解决方法:)
你知道你的类的私有方法的名称。例如,它们就像 _add()、_edit()、_delete() 等。
因此,当您想从单元测试方面对其进行测试时,只需通过为一些常用词添加前缀和/或后缀来调用私有方法(例如 _addPhpunit),以便在调用 __call() 方法时(因为方法 _addPhpunit() 不会存在)的所有者类,您只需将必要的代码放入 __call() 方法中以删除前缀/后缀单词/s(Phpunit),然后从那里调用该推导的私有方法。这是魔术方法的另一个很好的用途。
试试看。
替代方案。以下代码作为示例提供。它的实施可以更广泛。它的实现将帮助您测试私有方法并替换私有属性。
<?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
您可以在下面的代码中使用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!