4

我目前正在处理的项目包含面向对象和过程 PHP 代码的混合体。所以我有这样的事情:

function doStuff($value)
{
    $x = $value + 1;

    return $x;
}

class MyClass
{
    private $field;

    public function setMyValue($amount)
    {
        $this->field = doStuff($amount) + doStuff(2 * $amount);
    }
}

这些依赖项有一些,但很少(你可以用一只手数数)。但是,我需要为类编写单元测试(使用 PHPUnit),而且我不知道如何模拟来自程序端的函数(在本例中doStuff)。据我所见,PHPUnit 中的模拟功能仅适用于类。

我会在没有任何模拟的情况下完成它,但问题是其中一些函数会执行一些 IO 操作;我认为不以某种方式嘲笑它们不是一个好主意。

我该如何解决这个问题?

4

3 回答 3

5

当您从命名空间调用您的函数(在全局命名空间中定义)并始终调用它们时,您可以利用PHP 的命名空间回退策略

如果命名空间函数 […] 不存在,PHP 将回退到全局函数 […]。

这允许您通过在调用者的命名空间中提供函数来创建模拟。

为了让你的生活特别轻松,我将它打包到库php-mock-phpunit中,它可以与 PHPUnit 一起使用:

namespace foo;

use phpmock\phpunit\PHPMock;

class BuiltinTest extends \PHPUnit_Framework_TestCase
{

    use PHPMock;

    public function testTime()
    {
        $time = $this->getFunctionMock(__NAMESPACE__, "time");
        $time->expects($this->once())->willReturn(3);

        $this->assertEquals(3, time());
    }
}
于 2016-11-13T22:48:57.827 回答
1

我看到的唯一选择是依赖注入,因为您的班级想要使用班级外部的资源。因此,这打破了一些封装规则。

我过去是如何做到这一点的,是将这些函数放在它们自己的类中并要求/包含它们,并且在设置测试变量时,包含一个具有相同半“模拟”函数的基本文件,这些函数返回已知状态。

我这样做的另一种方法是创建一个简单的 UTILITY 类,其中包含所有这些数据函数,然后使用依赖注入和模拟来进行测试。

class Utilities
{

    function doStuff($value)
    {
        $x = $value + 1;
        return $x;
    }
}

class MyClass
{
    private $UtilitiesObject;

    private $field;

    public function setMyValue($amount)
    {
//        $this->field = doStuff($amount) + doStuff(2 * $amount);
        $this->field = $this->UtilitiesObject->doStuff($amount) + $this->UtilitiesObject->doStuff(2 * $amount);
    }
}

    // Constructor Injection, pass the Utilities object here
    public function __construct($Utilities = NULL)
    {
        if(! is_null($Utilities) )
        {
            if($Utilities instanceof Utilities)
            {
                $this->SetUtilities($Utilities);
            }
        }
    }

    function SetUtilities(Utilities $Utilities)
    {
        $this->UtilitiesObject = $Utilities
    }

}

测试:

class UtilitiesTest extends PHPUnit_Framework_TestCase
{

    // Could also use dataProvider to send different returnValues, and then check with Asserts.
    public function testSetMyValue()
    {
        // Create a mock for the Utilities class,
        // only mock the doStuff() method.
        $MockUtilities = $this->getMock('Utilities', array('doStuff'));

        // Set up the expectation for the doStuff() method 
        $MockUtilities->expects($this->any())
                    ->method('doStuff')
                    ->will($this->returnValue(1));

        // Create Test Object - Pass our Mock as the Utilities
        $TestClass = new MyClass($MockUtilities);
        // Or
        // $TestClass = new MyClass();
        // $TestClass->SetUtilitiess($MockUtilities);

        // Test doStuff
        $amount = 10;   // Could be checked with the Mock functions
        $this->assertEquals(2, $TestClass->doStuff($amount));       // Mock always returns 1, so 1+1=2
    }
}
于 2013-10-30T17:23:54.247 回答
0

您可以尝试重新定义“模拟”这些功能。

使用 PHP 库的一种选择 -拼凑

Patchwork\replace("size", function($x)
{
 return "huge";
});

或硬编码解决方案 - runkit 扩展(通过 PECL 安装) -

Runkit

bool runkit_function_redefine ( string $funcname , string $arglist , string $code )
于 2013-10-31T10:35:51.703 回答