【问题标题】:PHPUnit Mock Objects and Static MethodsPHPUnit 模拟对象和静态方法
【发布时间】:2010-03-01 15:49:00
【问题描述】:

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

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

理想情况下,我会使用一个模拟对象来确保调用fromArray(带有提供的用户数据)和save,但这是不可能的,因为该方法是静态的。

有什么建议吗?

【问题讨论】:

    标签: php unit-testing mocking doctrine phpunit


    【解决方案1】:

    PHPUnit 的作者 Sebastian Bergmann 最近发表了一篇关于 Stubbing and Mocking Static Methods 的博文。使用 PHPUnit 3.5 和 PHP 5.3 以及后期静态绑定的一致使用,您可以做到

    $class::staticExpects($this->any())
          ->method('helper')
          ->will($this->returnValue('bar'));
    

    更新: staticExpectsdeprecated as of PHPUnit 3.8,将在以后的版本中完全删除。

    【讨论】:

    • 值得注意的是“这种方法只适用于调用者和被调用者在同一个类中的静态方法调用的存根和模拟。这是因为static methods are death to testability。”
    • 自 PHPUnit v4 起,staticExpects 函数已被删除。请参阅this thread on github 了解原因。
    • 我们知道staticExpects 已经从最新版本的PHPUnit 中删除,那么没有staticExpects 的替代方法是什么?
    • @krishnaPrasad 查看链接的 github 问题。没有。
    • 很好的例子,说明无用的“优化”如何在现实世界中搞砸事情。 staticExpects() 是一个运行良好的工具,可以帮助数百万人编写适当的测试,但后来一些“聪明”的人决定删除它,因为他们知道别无选择,人们只能删除测试。即使是现在,7 年多之后,主要的框架和库仍然依赖于大量静态调用,这些调用现在无法测试。总结一下:我们想要编写测试和模拟外部库,但不能,因为 PHPUnit 的开发人员决定删除它。结果:根本没有测试。谢谢
    【解决方案2】:

    现在有 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'); 
    

    【讨论】:

    • 这个图书馆是黄金!但我认为他们应该在他们的页面上放一个免责声明:“仅仅因为您可以使用我们的库测试全局函数和静态方法,这并不意味着您应该以这种方式编写新代码。”我在某处读到,糟糕的测试总比没有测试要好,使用这个库,您可以为遗留代码添加安全网。只要确保以更好的方式编写新代码:)
    【解决方案3】:

    我会在单元测试命名空间中创建一个新类,扩展Model_User 并对其进行测试。这是一个例子:

    原课:

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

    要在单元测试中调用的模拟类:

    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 库来为我做这件事时,我会使用此方法。
    【解决方案4】:

    doublit 库还可以帮助您测试静态方法:

    /* Create a mock instance of your class */
    $double = Doublit::mock_instance(Model_User::class);
    
    /* Test the "create" method */
    $double::_method('create')
       ->count(1) // test that the method is called once
       ->args([Constraints::isInstanceOf('array')]) // test that first argument is an array
       ->stub('my_value') // stub the method to return "myvalue"
    

    【讨论】:

      【解决方案5】:

      找到了可行的解决方案,尽管主题很旧,但仍愿意分享。 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 
         }
      }
      

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

      【讨论】:

      • 主题的年龄无关紧要,如果您有一些有用的内容要添加,您应该始终发布有关旧主题的答案。
      【解决方案6】:

      另一种可能的方法是使用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')));
      

      【讨论】:

      • 目前没有稳定版本:\
      【解决方案7】:

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

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


      例如,这样的事情不能解决问题吗:

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

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

      【讨论】:

      • 感谢您的建议,它比任何东西都更有风格。我可以在这个特定的实例中使该方法非静态(尽管我更希望能够在不实例化的情况下使用它)。
      • 这个问题肯定是关于模拟静态方法——告诉作者“不要使用静态方法”并不能解决问题。
      【解决方案8】:

      还有一个approach

      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');
      }
      

      【讨论】:

      猜你喜欢
      • 2010-09-25
      • 1970-01-01
      • 1970-01-01
      • 2021-03-27
      • 1970-01-01
      • 2011-03-14
      • 2011-10-28
      • 2016-09-13
      • 1970-01-01
      相关资源
      最近更新 更多