如何为抽象类的模拟对象设置受保护的属性?

5

我找到了一个适用于普通对象的解决方案,但似乎不适用于模拟对象。

下面的测试失败,并显示以下消息:无法设置 Mock_ClassToTest_40ea0b83 对象类型的 someProperty 属性:属性 someProperty 不存在

class sampleTestClass extends PHPUnit_Framework_TestCase
{
    function test() {
        $object = $this->getMockForAbstractClass(ClassToTest::class, [], '', false);
        $this->setProtectedProperty($object, 'someProperty', 'value');
    }

    private function getReflectionProperty($object, $property) {
        $reflection         = new ReflectionClass($object);
        $reflectionProperty = $reflection->getProperty($property);
        $reflectionProperty->setAccessible(true);
        return $reflectionProperty;
    }

    /**
     * This method modifies the protected properties of any object.
     * @param object $object   The object to modify.
     * @param string $property The name of the property to modify.
     * @param mixed  $value    The value to set.
     * @throws TestingException
     */
    function setProtectedProperty(&$object, $property, $value) {
        try {
            $reflectionProperty = $this->getReflectionProperty($object, $property);
            $reflectionProperty->setValue($object, $value);
        }
        catch ( Exception $e ) {
            throw new TestingException("Unable to set property {$property} of object type " . get_class($object) .
                                       ': ' . $e->getMessage(), 0, $e);
        }
    }
}

abstract class ClassToTest
{
    private $someProperty;
    abstract function someFunc();
}

class TestingException extends Exception
{
}

编辑:2016年8月31日下午4:32 EST 根据Katie的回答更新了代码。


为什么要测试抽象类? - dm03514
这样我就不必为扩展它们的类编写额外的测试用例了。 - Michael
1
你仍然需要为继承抽象类的类编写测试用例。它们实现了基类的抽象函数,没有办法通过测试基类来测试这些函数。 - axiac
同意@axiac的观点,你可能能够测试抽象类的某些部分,但最终仍然需要测试扩展它的类。 - Katie
我有一堆抽象类,它们只有一个或两个抽象方法。其中一些方法非常简单,需要很少或根本不需要测试。 - Michael
2个回答

3
您正在尝试在模拟对象上调用反射方法,相反,您可以在抽象类本身上调用它:

因此,请更改为:

$reflection = new ReflectionClass(get_class($object));

to

$reflection = new ReflectionClass(ClassToTest::class);

这将适用于类中不是抽象的任何内容,例如您的属性或另一个完全实现的方法。

附注:因为 OP 被更新了,所以还需补充说明

该修复方法仍可适用于 getReflectionProperty 中的第一行。但如果您无法访问类名,则会出现问题。


那确实可以工作,但这只是我使用的实际代码的简化版本。在真实情况下,类名不可用。我已经更新了原帖。 - Michael
我有一个方法,接受一个对象、属性名称和属性值,并相应地设置对象的属性。我不想改变该方法的签名。 - Michael
你需要类名来正确反射类,从PHPUnit获取它会产生以下错误:无法设置对象类型为Mock_ClassToTest_27fdf591的属性someProperty:属性someProperty不存在。对象类型与你的类不同。 - Katie
总的来说,这可能不是一个好主意,但我想向你展示如何做到这一点。为测试而模拟抽象类可能会带来很多烦恼,但这取决于具体情况以及类有多少抽象部分或方法之间有多大的相互依赖性。 - Katie

3
使用反射来访问类的受保护和私有属性及方法在测试中似乎是一种非常聪明的方法,但会导致难以阅读和理解的测试。
另一方面,仅应测试类的公共接口。测试(甚至关心)已测试类的受保护和私有属性及方法表明测试是在代码之后编写的。这样的测试是脆弱的;已测试类实现的任何更改都将破坏测试,即使它没有破坏类的功能。
通常不需要测试抽象类。大多数情况下,其子类的测试也覆盖了抽象类的相关代码。如果它们未覆盖其中的某些部分,则要么该代码在那里不需要,要么测试用例未覆盖所有边缘情况。
但是,有时需要为抽象类编写测试用例。在我看来,最好的方法是在包含测试用例的文件底部扩展抽象类,为所有抽象方法提供简单的实现并将此类用作 SUT 的SUT
大致如下:
class sampleTestClass extends PHPUnit_Framework_TestCase
{
    public function testSomething()
    {
        $object = new ConcreteImplementation();
        $result = $object->method1();
        self::assertTrue($result);
    }
}

class ConcreteImplementation extends AbstractClassToTest
{
    public function someFunc()
    {
        // provide the minimum implementation that makes it work
    }
}

你正在测试你发布的代码中的一个模拟对象。模拟对象不应该被测试,它们的作用是模拟 SUT 的协作者行为,这些行为不适合在测试中实例化。
协作者类在测试中被模拟的原因包括但不限于以下几点:
  • 创建困难;例如,当模拟类的构造函数需要许多参数或其他对象时;
  • 协作者是抽象类或接口;实际实现可能在编写测试和被测试类时甚至不存在;
  • 协作者的代码需要很长时间才能完成或需要额外的资源(磁盘空间、数据库连接、Internet 连接等);
  • 协作者的代码具有永久的副作用;这通常与前一个原因相结合。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接