PHPUnit模拟函数?

时间:2016-09-06 06:23:14

标签: php unit-testing mocking phpunit

我有一个有趣的场景,我需要定义一个函数,以便为另一个函数进行测试。我想测试的功能看起来像这样:

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遇到了这个问题并找到了解决方案。

谢谢!

5 个答案:

答案 0 :(得分:2)

从代码的轻微重构开始,使其更易于测试。

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 :(得分: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 :(得分: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 / not foo和baz / not baz则需要四个文件。这绝对是一个开箱即用的选项,我通常不会推荐,但在你的情况下,这可能是你最好的选择。

答案 3 :(得分:0)

这还够吗?

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

答案 4 :(得分:0)

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

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

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

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

/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创建符号链接(例如,如果作曲家在根目录中,则从/test/with-baz创建ln -s ../../vendor/bin/phpunit),以确保在两种情况下都运行相同版本的phpunit。

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

<强>替代

使用phpunit&#39; @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'));
    }
}