PHPUnit模拟对象和静态方法

66

我正在寻找测试以下静态方法的最佳方法(特别是使用Doctrine Model):

class Model_User extends Doctrine_Record
{
    public static function create($userData)
    {
        $newUser = new self();
        $newUser->fromArray($userData);
        $newUser->save();
    }
}
理想情况下,我会使用一个模拟对象来确保fromArray(使用提供的用户数据)和save被调用,但由于该方法是静态的,这是不可能的。任何建议?
8个回答

48
Sebastian Bergmann,PHPUnit的作者,最近在博客中提到了存根和模拟静态方法。通过使用PHPUnit 3.5和PHP 5.3以及一致使用后期静态绑定,您可以实现此操作。
$class::staticExpects($this->any())
      ->method('helper')
      ->will($this->returnValue('bar'));

更新:staticExpects已经自PHPUnit 3.8起被弃用,并将在以后的版本中完全删除。


19
值得注意的是,“该方法仅适用于桩和模拟静态方法调用,其中调用方和被调用方位于同一类中。这是因为静态方法对可测试性没有帮助。” - Brad Koch
2
staticExpects 函数已在 PHPUnit v4 中被移除。请参阅 github 上的此线程 以了解原因。 - Arnold Daniels
8
众所周知,最近版本的PHPUnit已经完全移除了staticExpects,那么在没有staticExpects的情况下,有什么替代方法来实现这个功能呢? - krishna Prasad
51
一个关于“优化”在现实世界中是如何搞砸事情的好例子。staticExpects()是一个良好运作的工具,帮助数百万人编写适当的测试,但然后一些“聪明”的人决定将其删除,知道没有替代方案,人们只能删除测试。即使是在7年后的现在,主要的框架和库仍依赖许多静态调用,这些调用现在无法进行测试。总之:我们想编写测试和模拟外部库,但无法实现,因为PHPUnit的开发者决定将其删除。结果是:没有任何测试。谢谢。 - Sliq
1
这个答案在2022年似乎已经没有用了,尽管它有最多的投票。我认为应该删除它,以免妨碍有用的答案。 - Ian Dunn
显示剩余3条评论

15

现在有AspectMock库来帮助解决这个问题:

https://github.com/Codeception/AspectMock

$this->assertEquals('users', UserModel::tableName());   
$userModel = test::double('UserModel', ['tableName' => 'my_users']);
$this->assertEquals('my_users', UserModel::tableName());
$userModel->verifyInvoked('tableName'); 

11
这个库真是太棒了!但我认为他们应该在页面上加上一份免责声明:“仅仅因为你可以使用我们的库测试全局函数和静态方法,并不意味着你应该用这种方式编写新代码。” 我曾经在某个地方读到,一个糟糕的测试总比没有测试要好,而有了这个库,你可以给你的旧代码添加一个安全保障。只需确保以更好的方式编写新代码就可以了 :) - pedromanoel
1
使用AspectMock模拟静态函数/方法的模拟在测试中是持久的。一旦您在第一个测试中模拟了静态方法并在下一个测试中调用它,它将使用先前测试的模拟。在同一个类中甚至在与第一个测试类完全分离的另一个测试类中。这使得在实际世界中难以利用,因为您的测试可能在分离中工作,但在运行多个测试(即测试套件)时开始失败。有一种解决方法是对使用AspectMock模拟静态函数的测试使用@runInSeparateProcess注释-这远非理想! - Petr Urban

8

我将在单元测试命名空间中创建一个新的类,该类继承Model_User,并对其进行测试。这里是一个例子:

原始类:

class Model_User extends Doctrine_Record
{
    public static function create($userData)
    {
        $newUser = new self();
        $newUser->fromArray($userData);
        $newUser->save();
    }
}

在单元测试中调用的 Mock 类:

use \Model_User
class Mock_Model_User extends Model_User
{
    /** \PHPUnit\Framework\TestCase */
    public static $test;

    // This class inherits all the original classes functions.
    // However, you can override the methods and use the $test property
    // to perform some assertions.
}

在你的单元测试中:

use Module_User;
use PHPUnit\Framework\TestCase;

class Model_UserTest extends TestCase
{
    function testCanInitialize()
    {   
        $userDataFixture = []; // Made an assumption user data would be an array.
        $sut = new Mock_Model_User::create($userDataFixture); // calls the parent ::create method, so the real thing.

        $sut::test = $this; // This is just here to show possibilities.

        $this->assertInstanceOf(Model_User::class, $sut);
    }
}

当我不想使用额外的PHP库来完成它时,我会使用这种方法。 - b01

4

我找到了可行的解决方案,尽管这个话题已经很老了,但我还是想分享一下。 class_alias 可以替代尚未自动加载的类(仅在使用自动加载而非直接包含/需要文件时有效)。 例如,我们的代码:

class MyClass
{
   public function someAction() {
      StaticHelper::staticAction();
   }
}

我们的测试:

class MyClassTest 
{
   public function __construct() {
      // works only if StaticHelper is not autoloaded yet!
      class_alias(StaticHelperMock::class, StaticHelper::class);
   }

   public function test_some_action() {
      $sut = new MyClass();
      $myClass->someAction();
   }
}

我们的模拟:

class StaticHelperMock
{
   public static function staticAction() {
      // here implement the mock logic, e.g return some pre-defined value, etc 
   }
}

这个简单的解决方案不需要任何特殊的库或扩展。

3
主题的年龄并不重要,如果您有有用的内容可以添加,您应该始终在旧主题上发布答案。 - Ian Dunn
如何模拟一个单独的静态方法并在其余部分中使用真实实现?虽然可以模拟整个类。 - Jimmy
用这种方法,我认为不可能。 - Ilia Yatsenko

3

0
另一种可能的方法是使用Moka库:
$modelClass = Moka::mockClass('Model_User', [ 
    'fromArray' => null, 
    'save' => null
]);

$modelClass::create('DATA');
$this->assertEquals(['DATA'], $modelClass::$moka->report('fromArray')[0]);
$this->assertEquals(1, sizeof($modelClass::$moka->report('save')));

1
目前没有稳定版本:\ - Nebulosar

-1

再来一个方法

class Experiment
{
    public static function getVariant($userId, $experimentName) 
    {
        $experiment = self::loadExperimentJson($experimentName):
        return $userId % 10 > 5;  // some sort of bucketing
    } 

    protected static function loadExperimentJson($experimentName)
    {
        // ... do something
    }
}

在我的ExperimentTest.php文件中。
class ExperimentTest extends \Experiment
{
    public static function loadExperimentJson($experimentName) 
    {
        return "{
            "name": "TestExperiment",
            "variants": ["a", "b"],
            ... etc
        }"
    }
}

然后我会这样使用它:

public function test_Experiment_getVariantForExperiment()
{
    $variant = ExperimentTest::getVariant(123, 'blah');
    $this->assertEquals($variant, 'a');

    $variant = ExperimentTest::getVariant(124, 'blah');
    $this->assertEquals($variant, 'b');
}

这似乎是 https://dev59.com/rHE95IYBdhLWcg3wV8W_#47377592 的重复。 - Ian Dunn

-2

测试静态方法通常被认为有点困难(你可能已经注意到了),特别是在 PHP 5.3 之前。

你能不能修改你的代码,不使用静态方法?实际上,我不太明白为什么你要在这里使用静态方法;事实上,这可能可以重写为一些非静态代码,不是吗?


例如,像这样的东西不行吗:

class Model_User extends Doctrine_Record
{
    public function saveFromArray($userData)
    {
        $this->fromArray($userData);
        $this->save();
    }
}

不确定你将要测试什么;但是,至少不再有静态方法了...


谢谢您的建议,这更多地是关于风格。在这种特定情况下,我可以使方法非静态(尽管我更喜欢无需实例化即可使用它)。 - rr.
30
这个问题肯定是关于嘲笑静态方法的——告诉作者“不要使用静态方法”并不能解决问题。 - Lotus

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