PHPUnit模拟函数?

20

我有一个有趣的情况需要定义一个函数来测试另一个函数。我想要测试的函数看起来像这样:

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 包,也不能被项目以其他方式所需。显然,Mockery 也行不通。我的问题是,是否有更有经验的 PHPUnit 用户遇到过这个问题并找到了解决方法。
谢谢!

1
你觉得为测试命名空间化你的函数怎么样?我不完全确定你的测试函数实现是什么,但是可以参考这个链接:https://dev59.com/cXA65IYBdhLWcg3wzyFl#12128017 - Unamata Sanatarai
问题在于我需要定义一个不存在的函数来测试依赖它的函数,我不需要改变现有函数的实现方式。@UnamataSanatarai - samrap
如果你的示例中加上了namespace,我可以使用php-mock-phpunit来回答你。否则,你可以尝试使用runkit。 - Markus Malkusch
@GarethParker 这正是我一直在思考的。但是,我在引导文件中模拟的函数确实已经在测试中定义了,但是我正在测试的代码只有在定义了 foo 的情况下才定义 baz,但仍然没有定义 baz。今晚稍后会跟进。 - samrap
真正的问题是:为什么你要使用 baz 而不是 bar - Infiltrator
显示剩余3条评论
5个回答

3

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

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

对于你所要求的内容来说,这样已经足够了。不过,你仍然可以进一步重构这段代码,将函数生成提取为代码生成器。这样你就可以编写单元测试来确保它创建了你期望的函数类型。


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

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


1
这个足够了吗?

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,它通过自动加载处理文件包含。 - samrap
也许你应该展示一些真实的代码……你所包含的都是全局函数,因此对于这个特定的函数baz,我并不清楚自动加载是否会起作用。 - Justin McAleer

1
似乎这是一个需要模拟可见性的情况,这显然超出了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,或者四个文件,如果你想测试foo/非foo和baz/非baz。这绝对是一个非常创新的选择,在正常情况下我不会推荐,但在你的情况下,这可能是最好的选择。


这与在单独的引导文件中定义没有什么区别,我已经尝试过了,但并没有起作用。今晚我会继续深入研究,并更新我的问题以提供更多信息。 - samrap
听起来不错,很难理解你正在测试的环境。我在想你可以用 require 来替换你的示例代码(if 语句),然后引入你要测试的任何内容。祝好运! - Katie
@samrap 再说一件事,测试用例通过了且没有错误,希望能帮到你! - Katie
1
是的,就像我说的,今晚我需要再深入挖掘一下。谢谢你的帮助!我很快会跟进。 - samrap

0

将其添加到引导文件中为什么不起作用?

是因为有时系统会正确创建函数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和未定义函数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'));
    }
}

1
A和B不适用。我不关心foo的功能,只需要它返回一个静态值(foo不是我的函数,因此我的测试不关心它的工作原理)。我的引导文件与您的示例完全相同,但该函数似乎仍未在我正在测试的代码中定义。此时可能是PHPUnit与自动加载方式的问题。我需要今晚稍后查看autoload-dev并回复您。 - samrap
1
好的。听起来有点奇怪。上面的 SomeTest 确实通过了,我也检查了使用引导程序方法时的情况。期待听到您的发现。 - Kafoso

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