【问题标题】:PHPUnit mock function?PHPUnit 模拟函数?
【发布时间】:2017-01-13 11:47:37
【问题描述】:

我有一个有趣的场景,我需要定义一个函数来测试另一个函数。我要测试的函数如下所示:

if (function_exists('foo') && ! function_exists('baz')) {
    /**
     * Baz function
     * 
     * @param integer $n
     * @return integer
     */
    function baz($n)
    {
        return foo() + $n;
    }
}

我检查foo 是否存在的原因是因为它可能会或可能不会在开发人员的项目中定义,并且函数baz 依赖于foo。因此,如果baz 可以调用foo,我只想定义它。

唯一的问题是到目前为止还无法为其编写测试。我尝试在 PHPUnit 配置中创建一个引导脚本,该脚本将定义一个假的 foo 函数,然后需要 Composer 自动加载器,但我的主脚本仍然认为 foo 未定义。 foo 不是 Composer 包,因此我的项目不需要。显然,嘲弄也不会为此工作。我的问题是,是否有任何对 PHPUnit 更有经验的人遇到过这个问题并找到了解决方案。

谢谢!

【问题讨论】:

  • 如何为测试函数命名空间?我不是 100% 知道您对测试功能的实现是什么,但是:stackoverflow.com/a/12128017/2119863
  • 问题是我需要定义一个不存在的函数来测试依赖于它的函数@UnamataSanatarai。我不需要更改现有功能的实现。
  • 如果我可以在您的示例中添加一个namespace,我会用php-mock-phpunit 给您一个答案。否则就选择 runkit 之类的东西。
  • Grrr。 php-mock-phpunit 看起来很棒,但不幸的是,我需要模拟的函数位于根名称空间中(来自 WordPress 插件;是的,不要问我为什么使用 WordPress -__-)所以我会看看 runkit 跨度>
  • 你根本无法模拟这种程序代码。我要么将这些东西包装在一个可模拟的类中,要么使用(环境)定义,如 C 风格的标题保护。

标签: php unit-testing mocking phpunit


【解决方案1】:

这应该可行!

use MyProject\baz;

class YourTestCase
{
    /** @var callable **/
    protected $mockFoo;

    /** @var callable **/
    protected $fakeFoo;

    public function setUp()
    {
        if (function_exists('foo')) {
            $this->mockFoo = function($foosParams) {
                foo($foosParams);
                // Extra Stuff, as needed to make the test function right.
            };
        }

        $this->fakeFoo = function($foosParams) {
            // Completely mock out foo.
        };
    }

    public function testBazWithRealFoo()
    {
        if (!$this->mockFoo) {
            $this->markTestIncomplete('This system does not have the "\Foo" function.');
        }

        $actualResults = baz($n, $this->mockFoo);
        $this->assertEquals('...', $actualResults);
    }

    public function testBazWithMyFoo()
    {
        $actualResults = baz($n, $this->fakeFoo);
        $this->assertEquals('...', $actualResults);
    }
}

然后修改你现有的代码:

if (function_exists('foo') && ! function_exists('baz')) {
    /**
     * Baz function
     * 
     * @param integer $n
     * @return integer
     */
    function baz($n)
    {
        return foo() + $n;
    }
}

namespace MyProject
{
    function baz($bazParams, callable $foo = '\foo')
    {
        return $foo() + $bazParams;
    }
}

那么你需要调用而不是调用baz($n)

use MyProject\baz;
baz($bazParams);

这就像函数的依赖注入,哟;-)

【讨论】:

    【解决方案2】:

    这就够了吗?

    to-test.php:

    <?php
    if (function_exists('foo') && ! function_exists('baz')) {
        /**
         * Baz function
         *
         * @param integer $n
         * @return integer
         */
        function baz($n)
        {
            return foo() + $n;
        }
    }
    

    BazTest.php:

    <?php
    
    class BazTest extends PHPUnit_Framework_TestCase {
        public function setUp()
        {
            function foo()
            {
                // Appropriate mock goes here
                return 1;
            }
    
            include __DIR__ . '/to-test.php';
        }
    
        public function testBaz()
        {
            $this->assertEquals(2, baz(1));
            $this->assertEquals(3, baz(2));
        }
    }
    

    运行测试产生:

    PHPUnit 5.4.8 by Sebastian Bergmann and contributors.
    
    .                                                                   1 / 1 (100%)
    
    Time: 54 ms, Memory: 8.00MB
    
    OK (1 test, 2 assertions)
    

    【讨论】:

    • 很遗憾没有。我正在使用 Composer,它通过自动加载处理文件包含。
    • 也许你应该展示一些你的真实代码......你包含的是所有全局函数,因此我不清楚自动加载是否会针对这个特定的函数起作用 baz。跨度>
    【解决方案3】:

    在引导文件中添加它不起作用 - 为什么?

    是不是因为有时函数baz 是(A)由系统正确创建的,有时(B)你需要模拟它?或者 (C) 你总是需要模拟它吗?

    • 案例 A:为什么代码偶尔会在运行中创建重要功能?
    • 案例 B:一个函数只能注册一次,不能取消注册或覆盖。因此,您要么选择模拟,要么不选择。不允许混合。
    • 案例 C:如果您总是需要模拟它,并且将其添加到引导文件中,它将被定义。关于您的尝试,您的 phpunit 引导文件未正确加载,或者您拼错了函数的名称。

    我确定你已经正确配置了你的 phpunit 引导,但是为了更好的衡量,它看起来像下面这样吗:

    /tests/phpunit.xml:

    <phpunit
        bootstrap="phpunit.bootstrap.php"
    </phpunit>
    

    /tests/phpunit.bootstrap.php:

    <?php
    require(__DIR__ . "/../bootstrap.php"); // Application startup logic; this is where the function "baz" gets defined, if it exists
    
    if (function_exists('foo') && ! function_exists('baz')) {
        /**
         * Baz function
         * 
         * @param integer $n
         * @return integer
         */
        function baz($n)
        {
            return foo() + $n;
        }
    }
    

    不要在测试中即时创建函数 baz,例如在setUp 函数中。

    phpunit 中的测试套件使用相同的引导程序。因此,如果您需要测试定义了函数baz 的情况以及未定义函数的其他情况(并且需要模拟它),则需要拆分tests 文件夹,例如在两个不同的文件夹中,每个文件夹都有 phpunit.xmlphpunit.bootstrap.php 文件。例如。 /tests/with-baz/tests/mock-baz。从这两个文件夹中,分别运行测试。只需在每个子文件夹中创建指向phpunit 的符号链接(例如,如果composer 在根目录中,则从/test/with-baz 创建ln -s ../../vendor/bin/phpunit)以确保在两种情况下运行相同版本的phpunit。

    当然,最终的解决方案是找出 baz 函数的定义位置,并尽可能手动包含罪魁祸首脚本文件,以确保应用正确的逻辑。

    替代方案

    使用phpunit的@runInSeparateProcess注解,根据需要定义函数。

    <?php
    class SomeTest extends \PHPUnit_Framework_TestCase
    {
        /**
         * @runInSeparateProcess
         */
        public function testOne()
        {
            if (false === function_exists('baz')) {
                function baz() {
                    return 42;
                }
            }
            $this->assertSame(42, baz());
        }
    
        public function testTwo()
        {
            $this->assertFalse(function_exists('baz'));
        }
    }
    

    【讨论】:

    • A 和 B 不适用。我不关心foo 的功能,只需要它返回一个静态值(foo 不是我的函数,所以我的测试不关心它是如何工作的)。我的引导文件看起来与您的示例相同,但该函数似乎仍未在我正在测试的代码中定义。此时,PHPUnit 使用自动加载的方式可能存在问题。我需要在今晚晚些时候查看autoload-dev 并回复您。
    • 好的。这听起来很奇怪。上面的SomeTest 确实通过了,我在使用引导方法时也进行了检查。期待听到您的发现。
    【解决方案4】:

    似乎这是您需要模拟可见性的情况,这绝对超出了类库 PHPUnit 的范围。因此,您仍然可以使用 PHPUnit,但可能必须跳出它来实现您的目标。

    我能想到的创建函数模拟的唯一方法是在测试用例中,在扩展 \PHPUnit_Framework_TestCase 的对象上方。

    function foo() {
        return 1;
    }
    
    if (function_exists('foo') && ! function_exists('baz')) {
        /**
         * Baz function
         *
         * @param integer $n
         * @return integer
         */
        function baz($n)
        {
            return foo() + $n;
        }
    }
    
    
    class TestBaz extends \PHPUnit_Framework_TestCase
    {
    
        public function test_it() {
    
            $this->assertSame(4,baz(3));
        }
    
    
    }
    

    您可能必须创建两个文件,一个带有 foo,一个不带,或者如果您想测试 foo/not foo 和 baz/not baz,则需要创建四个。这绝对是一个开箱即用的选项,我通常不会推荐,但在你的情况下,这可能是你最好的选择。

    【讨论】:

    • 这与在单独的引导文件中定义它没有什么不同,我已经尝试过但无法正常工作。今晚我将进行更多挖掘,并用更多信息更新我的问题。
    • 听起来不错,很难理解您正在测试的环境。我在想你可以用 require 替换你的示例代码(if 语句),然后将你正在测试的任何东西拉进去。祝你好运!
    • @samrap 再快一点,测试用例确实通过了,没有错误,希望对您有所帮助!
    • 是的,就像我说的,今晚我将不得不挖掘更多。谢谢你的帮助!我会尽快跟进
    【解决方案5】:

    首先对代码进行轻微重构,使其更易于测试。

    function conditionalDefine($baseFunctionName, $defineFunctionName)
    {
        if(function_exists($baseFunctionName) && ! function_exists($defineFunctionName))
        {
            eval("function $defineFunctionName(\$n) { return $baseFunctionName() + \$n; }");
        }
    }
    

    那就这样称呼它吧:

    conditionalDefine('foo', 'bar');
    

    您的 PHPUnit 测试类将包含以下测试:

    public function testFunctionIsDefined()
    {
        $baseName = $this->mockBaseFunction(3);
        $expectedName = uniqid('testDefinedFunc');
        conditionalDefine($baseName, $expectedName);
        $this->assertTrue(function_exists($expectedName));
        $this->assertEquals(5, $expectedName(2)); 
    }
    public function testFunctionIsNotDefinedBecauseItExists()
    {
        $baseName = $this->mockBaseFunction(3);
        $expectedName = $this->mockBaseFunction($value = 'predefined');
        conditionalDefine($base, $expectedName);
        // these are optional, you can't override a func in PHP
        // so all that is necessary is a call to conditionalDefine and if it doesn't
        // error, you're in the clear
        $this->assertTrue(function_exists($expectedName));
        $this->assertEquals($value, $expectedName()); 
    }
    public function testFunctionIsNotDefinedBecauseBaseFunctionDoesNotExists()
    {
        $baseName = uniqid('testBaseFunc');
        $expectedName = uniqid('testDefinedFunc');
        conditionalDefine($base, $expectedName);
        $this->assertFalse(function_exists($expectedName));     
    }
    
    protected function mockBaseFunction($returnValue)
    {
        $name = uniqid('testBaseFunc');
        eval("function $name() { return '$value'; }");
        return $name;
    }
    

    这足以满足您的要求。但是,您可以进一步重构此代码,将函数生成提取到代码生成器中。您可以针对生成器编写单元测试以确保它创建您期望的那种功能。

    【讨论】:

      猜你喜欢
      • 2016-01-30
      • 2015-10-03
      • 2020-05-08
      • 2015-12-18
      • 2013-02-23
      • 1970-01-01
      • 1970-01-01
      • 2015-07-10
      • 2017-02-04
      相关资源
      最近更新 更多