【问题标题】:phpunit - mockbuilder - set mock object internal propertyphpunit - mockbuilder - 设置模拟对象内部属性
【发布时间】:2013-09-04 16:12:21
【问题描述】:

是否可以创建一个禁用构造函数并手动设置受保护属性的模拟对象?

这是一个愚蠢的例子:

class A {
    protected $p;
    public function __construct(){
        $this->p = 1;
    }

    public function blah(){
        if ($this->p == 2)
            throw Exception();
    }
}

class ATest extend bla_TestCase {
    /** 
        @expectedException Exception
    */
    public function testBlahShouldThrowExceptionBy2PValue(){
        $mockA = $this->getMockBuilder('A')
            ->disableOriginalConstructor()
            ->getMock();
        $mockA->p=2; //this won't work because p is protected, how to inject the p value?
        $mockA->blah();
    }
}

所以我想注入受保护的 p 值,所以我不能。我应该定义 setter 还是 IoC,或者我可以用 phpunit 来做?

【问题讨论】:

  • 仅作记录 - 如果您正在测试非公共 API,那么您做错了。单元测试是关于测试行为,而不是内部实现。

标签: php phpunit


【解决方案1】:

根据@gontrollez 接受的答案,由于我们使用的是模拟构建器,因此无需调用new A;,因为我们可以使用类名。

    $a = $this->getMockBuilder(A::class)
        ->disableOriginalConstructor()
        ->getMock();

    $reflection = new ReflectionClass(A::class);
    $reflection_property = $reflection->getProperty('p');
    $reflection_property->setAccessible(true);

    $reflection_property->setValue($a, 2);

【讨论】:

    【解决方案2】:

    基于上面的@rsahai91 答案,创建了一个新的帮助程序,用于使多种方法可以访问。可以是私有的或受保护的

    /**
     * Makes any properties (private/protected etc) accessible on a given object via reflection
     *
     * @param $object - instance in which properties are being modified
     * @param array $properties - associative array ['propertyName' => 'propertyValue']
     * @return void
     * @throws ReflectionException
     */
    public function setProperties($object, $properties)
    {
        $reflection = new ReflectionClass($object);
        foreach ($properties as $name => $value) {
            $reflection_property = $reflection->getProperty($name);
            $reflection_property->setAccessible(true);
            $reflection_property->setValue($object, $value);
        }
    }
    

    使用示例:

    $mock = $this->createMock(MyClass::class);
    
    $this->setProperties($mock, [
        'propname1' => 'valueOfPrivateProp1',
        'propname2' => 'valueOfPrivateProp2'
    ]);
    

    【讨论】:

      【解决方案3】:

      如果每个代码库都使用 DI 和 IoC,并且从未做过这样的事情,那就太棒了:

      public function __construct(BlahClass $blah)
      {
          $this->protectedProperty = new FooClass($blah);
      }
      

      当然,您可以在构造函数中使用模拟 BlahClass,但随后构造函数会将受保护的属性设置为您无法模拟的内容。

      所以您可能在想“重构构造函数以采用 FooClass 而不是 BlahClass,那么您不必在构造函数中实例化 FooClass,而是可以放入一个模拟!”好吧,你是对的,如果这并不意味着你必须改变整个代码库中类的每一个用法,给它一个 FooClass 而不是 BlahClass。

      并非每个代码库都是完美的,有时您只需要完成工作即可。这意味着,是的,有时您需要打破“仅测试公共 API”的规则。

      【讨论】:

      • ->disableOriginalConstructor ?
      【解决方案4】:

      我想留下一个方便的帮助方法,可以在这里快速复制和粘贴:

      /**
       * Sets a protected property on a given object via reflection
       *
       * @param $object - instance in which protected value is being modified
       * @param $property - property on instance being modified
       * @param $value - new value of the property being modified
       *
       * @return void
       */
      public function setProtectedProperty($object, $property, $value)
      {
          $reflection = new ReflectionClass($object);
          $reflection_property = $reflection->getProperty($property);
          $reflection_property->setAccessible(true);
          $reflection_property->setValue($object, $value);
      }
      

      【讨论】:

        【解决方案5】:

        您可以使用反射将属性公开,然后设置所需的值:

        $a = new A;
        $reflection = new ReflectionClass($a);
        $reflection_property = $reflection->getProperty('p');
        $reflection_property->setAccessible(true);
        
        $reflection_property->setValue($a, 2);
        

        无论如何,在您的示例中,您不需要为要引发的异常设置 p 值。您正在使用模拟来控制对象的行为,而无需考虑其内部结构。

        因此,不是设置 p = 2 来引发异常,而是将模拟配置为在调用 blah 方法时引发异常:

        $mockA = $this->getMockBuilder('A')
                ->disableOriginalConstructor()
                ->getMock();
        $mockA->expects($this->any())
                 ->method('blah')
                 ->will($this->throwException(new Exception));
        

        最后,奇怪的是你在 ATest 中模拟 A 类。您通常模拟您正在测试的对象所需的依赖项。

        希望这会有所帮助。

        【讨论】:

        • A 类没有完全依赖注入,我在它的构造函数中创建了几个类的新实例......所以我需要重写构造函数来模拟这些实例。不是最好的方法,我想我会使用依赖注入容器来代替这个。
        • 你的代码肯定会更容易测试。实现 DI 有多种选择,但这是一个非常简单的选择:pimple.sensiolabs.org
        • 不要在测试中使用依赖注入容器!一个好的单元测试只测试一个类,并且所有依赖项都作为完全配置的模拟注入。如果你不能做到这一点,那么你的架构很糟糕,应该改进。
        • 而不是$a->p = 2;,我不得不使用$reflection_property->setValue($a, 2),因为第一种方法返回错误Fatal error: Cannot access protected property Store_Api_Version_2_Resource_Products::$_fields in ...
        猜你喜欢
        • 2017-11-18
        • 1970-01-01
        • 2014-12-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-10-31
        • 2012-09-26
        相关资源
        最近更新 更多