【问题标题】:Why phpunit doesn't run __destruct() in mocked class and how to force it?为什么 phpunit 不在模拟类中运行 __destruct() 以及如何强制它?
【发布时间】:2013-01-29 06:41:06
【问题描述】:

代码会解释一切:

<?php

class ATest extends PHPUnit_Framework_TestCase
{
    public function testDestructorOnOriginalClass() {
        $a = new A();                                             // It
        unset($a);                                                // works
        echo " great!";                                           // great!
        $this->expectOutputString('It works great!');
    }

    public function testDestructorOnMockedClass() {
        $a = $this->getMock('A', array('someNonExistingMethod')); // It
        unset($a);                                                // works
        echo " great!";                                           // great!
        $this->expectOutputString('It works great!');
    }
}

class A {
    public function __construct()
    {
        echo "It";
    }

    public function __destruct()
    {
        echo " works";
    }
}

和输出:

# phpunit ATest.php 
PHPUnit 3.7.13 by Sebastian Bergmann.

.F

Time: 0 seconds, Memory: 3.50Mb

There was 1 failure:

1) ATest::testDestructorOnMockedClass
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'It works great!'
+'It great! works'


FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

正如您在第二个测试中看到的那样,它以错误的顺序打印 works,可能是因为 phpunit 在某处存储了对 mock 的引用,并且在测试结束时调用了 __destruct()...好吧,我已经检查过 @987654325 @ 方法,实际上它存储了对模拟对象 ($this-&gt;mockObjects[] = $mockObject;) 的引用,有效地阻止了对象被破坏,因此永远不会调用 __destructor()

/**
 * Returns a mock object for the specified class.
 *
 * @param  string  $originalClassName
 * @param  array   $methods
 * @param  array   $arguments
 * @param  string  $mockClassName
 * @param  boolean $callOriginalConstructor
 * @param  boolean $callOriginalClone
 * @param  boolean $callAutoload
 * @param  boolean $cloneArguments
 * @return PHPUnit_Framework_MockObject_MockObject
 * @throws PHPUnit_Framework_Exception
 * @since  Method available since Release 3.0.0
 */
public function getMock($originalClassName, $methods = array(), array $arguments = array(), $mockClassName = '', $callOriginalConstructor = TRUE, $callOriginalClone = TRUE, $callAutoload = TRUE, $cloneArguments = FALSE)
{
    $mockObject = PHPUnit_Framework_MockObject_Generator::getMock(
      $originalClassName,
      $methods,
      $arguments,
      $mockClassName,
      $callOriginalConstructor,
      $callOriginalClone,
      $callAutoload,
      $cloneArguments
    );

    $this->mockObjects[] = $mockObject;

    return $mockObject;
}

所以问题是 - 有没有办法防止这种情况发生?我认为在应该调用 __destruct() 时忽略它是不好的限制。

【问题讨论】:

  • 这是一个理论问题,还是您真的依赖 SUT 中依赖类的析构函数行为?如果是这样,这可能是糟糕设计的标志。这个问题不应该真的成为问题。
  • 目前我尝试模拟 PDO 类并模仿它在对象被破坏时结束数据库连接的行为(并且没有其他方法可以关闭连接) - 所以这不是我可以更改的类(除了扩展它,如果用 getMock() 模拟不起作用,我可能会这样做)。
  • 好吧,无论如何,PDO 是一个特例。如果你想模拟它,你需要先扩展它并覆盖构造函数,否则 MockBuilder 根本无法实例化它(假设你不想要一个实际的连接,否则你不会模拟它,你会吗?)
  • 是的,没错,但其中还有很多内容需要解释。我只是好奇我在模拟对象后是否缺少__destruct() 的这种行为。感谢您的帮助。

标签: php unit-testing mocking phpunit destructor


【解决方案1】:

问题是,当您不向“getMock”方法的第二个参数传递任何值时,PHPUnit 将存根您正在模拟的类中的所有方法(包括“__destruct”)。

但是如果你指定了至少一个方法(它甚至可能是不存在的方法)PHPUnit 将只存根你传入第二个参数的这些方法。

所以如果你想保留所有方法,但你也想创建模拟,你应该这样做:

$mock = $this->getMock('A', array('someNonExistingMethod'));

如果您更改此行,您的测试应该通过。

【讨论】:

  • 你的简单回答让我有点头疼:) 因为这修复了我提供的测试用例,但我的真实代码提供了这个非空参数,但代码仍然不起作用。我的测试用例的问题是动作发生的顺序 - 最后每个对象都被销毁,这就是这个测试用例在你的建议之后成功的方式。此外,即使PHPUnit_Framework_TestCase::mockObjects[] 中存在引用,我也无法相信会调用__destruct()。我已经更新了我的问题,你会明白我在说什么:)
  • 好的,我现在明白你的意思了。似乎PU在测试完成后发布模拟引用..我想不出任何方法来删除这个引用(我虽然首先 - 也许可以直接删除它但不幸的是 $mockObjects 不受保护但私有) , 对不起。但也许你应该小心在析构函数中保留任何逻辑。
  • 有时这并不取决于你,即使它会那么我仍然认为这是 phpunit 中的一个错误,因为它在不应该改变模拟类的行为时。目前我正在尝试模仿 PDO 行为,当对象被销毁时,它会终止与数据库的连接。可能很难更改 PDO 类的逻辑:) 可能我最终会用自定义类扩展这个类并将其用作模拟而不是 getMock() 创建的类。但是,如果能做到这种 phpunit 方式会更好。
【解决方案2】:

您只是取消设置局部变量 - 您并没有破坏对象本身。

该对象也由 PHPUnit 自己保存。所以仍然有一个参考,因此您的unset() 不会导致__destruct()

因此,当前的行为无法更改。在 phpunit 问题跟踪器中打开一个错误。

【讨论】:

  • 嗯,这是在我的问题中指出的,phpunit 保持引用。我在问是否可以避免这种情况,所以模拟的行为与模拟类完全一样。
【解决方案3】:

如果我们clone模拟对象和unset它,则可以在测试期间调用克隆对象的__destruct()。它可以帮助您测试__destruct()

请注意,此方法不能阻止原始模拟对象的__destruct()在测试结束时被调用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-18
    • 2020-09-17
    • 2019-04-15
    • 2015-02-04
    • 1970-01-01
    • 2020-09-14
    • 2021-09-13
    相关资源
    最近更新 更多