【问题标题】:PHP Unit / Mockery - Mock static function not workingPHP Unit / Mockery - 模拟静态函数不起作用
【发布时间】:2021-09-03 20:19:50
【问题描述】:

嘿,所以我有以下方法,当尝试模拟 Manager::getConfiguration 调用时,我无法让它工作。我尝试使用 namedMock 以及单元测试装饰器让它在另一个线程中运行。它要么给我一个类已经存在的错误,要么实际上提取了我不想要的管理器配置。

public function getBaseTypeManagerConfig() {
        $baseTypeManagerConfig = array();

        if (!empty($this->dataConfig)
                && !empty($baseType = $this->dataConfig->getBaseType())) {
            $baseTypeManagerConfig = Manager::getConfiguration($baseType);
        }

        return $baseTypeManagerConfig;
}

/**
 * @runTestsInSeparateProcesses
 * @preserveGlobalState disabled
 */
public function testBaseTypeManagerConfig() {
         $managerMock = Mockery::namedMock('Manager', 'ManagerStub');
         $managerMock->shouldReceive('getConfiguration')->andReturn(self::MOCKED_MANAGER_DECODED_JSON);

         $listModule = Mockery::mock('GenericListModule')->makePartial();
         $listModule->shouldReceive([
                 'getBaseType' => 'events',
         ]);

         $config = \SWL\Lib\Generic\ConfigFactory::getConfig('Events_Event');
         $listModule->shouldReceive('setDataConfig')->andSet('dataConfig', $config);

         $baseTypeManagerConfig = $listModule->getBaseTypeManagerConfig();
         $this->assertEquals(array(), $baseTypeManagerConfig);
}

// Attempt at using named mock to replace 'Manager' class with mock
class ManagerStub {
    public static function getConfiguration($configType) {
        return array();
    }
}

关于为什么我不能使用这个命名的模拟来替换 Manager 调用有什么想法吗?单元测试和 phpunit/mockery 的新手,因此欢迎任何其他指针。谢谢! :)

【问题讨论】:

    标签: php unit-testing phpunit


    【解决方案1】:

    感谢您的提问,欢迎来到 Stackoverflow。

    关于为什么我不能使用这个命名的模拟来替换 Manager 调用有什么想法吗?单元测试和 phpunit/mockery 的新手,因此欢迎任何其他指针。谢谢! :)

    这可能不是关于测试框架和模拟库的问题,而是由所讨论的语言(这里是 PHP)更好地回答,但它也可能类似地适用于其他语言。

    给定一个静态方法的调用,例如:

    $baseTypeManagerConfig = Manager::getConfiguration($baseType);
    

    没有什么可以动态替换,因为Manager::getConfiguration 是静态的。

    既然这很常见,为什么在为此类代码编写测试时会出现模拟的问题?

    由于一个类只能存在一次(其名称不是别名的约束,例如具有一个或多个不同名称的同一类),因此特定方法也只能定义一次。

    使用通用模拟框架(模拟生成器、存根生成器等)可能会出现问题。虽然可以创建经理类的模拟:

    Manager -> ManagerMock
    

    被测代码不会神奇地改变为它,例如像这样:

    $baseTypeManagerConfig = ManagerMock::getConfiguration($baseType);
    

    但保持不变:

    $baseTypeManagerConfig = Manager::getConfiguration($baseType);
    

    因此对模拟一无所知。

    这可能看起来您无法模拟Manager,但只是Manager 处于全局静态状态并且它保持不变。 Manager 的 mock 已添加到其中,但是为了您的测试目的而执行被测代码是没有用的。

    到目前为止,一切都很好。拥抱一下。


    此类问题的一个常见答案是使Manager 能够注入。

    这可以通过一个测试点,例如仅在允许您深入检查断言的测试中具有特定的 Manager 类。

    这也可能是依赖注入,即将Manager 作为依赖注入,例如在创建对象作为其构造函数的参数时。

    public function getBaseTypeManagerConfig() {
            $baseTypeManagerConfig = array();
    
            if (!empty($this->dataConfig)
                    && !empty($baseType = $this->dataConfig->getBaseType())) {
                $configurationGettingFunction = $this->configurationGettingFunction;
                $baseTypeManagerConfig = $configurationGettingFunction($baseType);
            }
    
            return $baseTypeManagerConfig;
    }
    

    它将Manager::getBaseTypeManagerConfig 的隐藏依赖项变为可见且可注入的依赖项。

    然后,在测试中,当创建被测对象 (SUT) 时,您注入“模拟”函数而不是静态函数。

    这也使 SUT 需要哪些协作者来完成其工作变得可见。您的测试涵盖了该重构。

    这应该很容易做到。

    Mind 认为这增加了一层间接性。虽然可能值得考虑它对您的设计有益,但也可能没有。

    因此,人们可能会认为间接是不需要的,Manager::getConfiguration 应该保持静态,并且打算使用静态方法调用(按设计)。

    然后它应该能够表示可测试的配置,因此虽然它保持静态,但它至少能够满足两种环境中的配置需求:开发和测试。

    开发环境只是默认环境,你的代码存在并执行。

    测试环境是您测试代码时的环境,例如在单元测试中。

    不管你最终如何解决,这两个环境你在编写测试时总会找到。

    并且只有与测试环境兼容的代码才能被测试。

    因此,您的目标是您的代码通常是可测试的(并且使用尽可能少的模拟和存根,即您编写的代码仅用于测试,然后您最重要的是测试您的模拟和存根,而不是代码你想测试)。

    这通常也是开始为代码编写测试时的方式,这揭示了设计问题,显示代码可能无法在不同环境中移植的地方。这可以:

    • 揭示缺失的配置功能(例如,通过根据需要注入数据(参数)使代码按预期工作)
    • 揭示更高层次的设计问题(例如,函数和类的使用不够灵活,无法按预期工作)

    这里的重要部分是,您在编写测试和代码时了解问题出在哪里,这使您难以编写测试。

    所以首先要意识到最重要的不是技术问题(为什么模拟库不支持功能 xyz?),而是要理解被测代码(它实际上在这里做什么?这是我的意图吗?)。

    只有后面的部分才能让您更改代码并进一步开发,并从编写测试中获得好处。

    否则你会写一个测试只是为了写一个并让它通过。没多大用处。使用能够实际覆盖Manager::getConfiguration - UopzComponere 的扩展程序非常容易。

    但是,在这里将其视为非技术问题,而应将其视为代码的设计问题。为什么?因为您从测试中获得了更多好处,并且您的代码将被更改。现在和将来。测试可以让您现在以可重现的方式查看这些变更需求,不仅是现在,而且在您开始编写测试时也可以提前查看。

    使用测试来定义代码的工作方式。不要创建模拟。

    如果您允许我发表个人评论:我讨厌在测试中编写模拟。即使对于技术上可以使用模拟的代码,它也很早就开始变得麻烦。因此,我经常看一下如何简化代码,以便更容易使用。这样它也可以更容易地进行测试。

    【讨论】:

    • 嘿 Hakre,非常感谢您的回复 - 这真的很有帮助。我正在一个大型系统中工作并尝试 TDD(我们目前没有单元测试)。这篇文章有助于让我停下来思考我实际需要测试什么等,并考虑整个产品的更大设计决策,而不是过于专注于单个测试。
    • @harrisonlucas:如果它回答了你的问题,你可以给它一个绿色的复选标记。如果它有帮助,请给它一个赞成票。欢迎再次使用 Stackoverflow。很高兴阅读它给了你很好的指导。祝测试愉快!
    • 想想你的评论,如果你想好好读一读,那可能是 Michael Feathers 的《有效地使用遗留代码》。当代码已经完成时,您想明确知道在哪里引入单元测试以及用于什么。你想知道边界。恕我直言,这本书可以激发一些非常有用的想法和实践。
    • 好的,太好了,我一定会检查那本书 - 再次感谢所有帮助!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-26
    • 2013-11-04
    • 2016-08-20
    • 2018-10-09
    • 1970-01-01
    • 2020-05-14
    相关资源
    最近更新 更多