如何在 PHP 中为单元测试伪造资源?

aut*_*tix 4 php phpunit unit-testing mocking fsockopen

我有一个方法,打开一个套接字连接,使用,然后关闭它。为了使其可测试,我将处理连接移到了单独的方法中(请参阅下面的代码)。

现在我想为 编写一个单元测试barIntrefaceMethod()并需要模拟方法openConnection()。换句话说,我需要一个假的resource.

是否有可能/如何resource在 PHP 中“手动”创建该类型的变量(为了伪造“打开的文件、数据库连接、图像画布区域等”等句柄)?


FooClass

class FooClass
{
    public function barIntrefaceMethod()
    {
        $connection = $this->openConnection();
        fwrite($connection, 'some data');
        $response = '';
        while (!feof($connection)) {
            $response .= fgets($connection, 128);
        }
        return $response;
        $this->closeConnection($connection);
    }

    protected function openConnection()
    {
        $errno = 0;
        $errstr = null;
        $connection = fsockopen($this->host, $this->port, $errno, $errstr);
        if (!$connection) {
            // TODO Use a specific exception!
            throw new Exception('Connection failed!' . ' ' . $errno . ' ' . $errstr);
        }
        return $connection;
    }

    protected function closeConnection(resource $handle)
    {
        return fclose($handle);
    }
}
Run Code Online (Sandbox Code Playgroud)

Joh*_*eph 5

根据我的评论,我认为您最好稍微重构一下代码,以消除对使用实际资源句柄实际调用的本机函数的依赖。您对 的测试所关心的FooClass::barInterfaceMethod只是它返回一个响应。那个类不需要关心资源是否被打开、关闭、写入等。所以这就是应该被模拟的。

下面我用一些简单的非生产伪代码重写了您在问题中的内容以进行演示:

你真正的班级:

class FooClass
{
    public function barInterfaceMethod()
    {
        $resource = $this->getResource();
        $resource->open($this->host, $this->port);
        $resource->write('some data');

        $response = '';
        while (($line = $resource->getLine()) !== false) {
            $response .= $line;
        }

        $resource->close();

        return $response;
    }

    // This could be refactored to constructor injection
    // but for simplicity example we will leave at this
    public function getResource()
    {
        return new Resource;
    }
}
Run Code Online (Sandbox Code Playgroud)

你对这门课的测试:

class FooClassTest
{
    public function testBarInterfaceMethod()
    {
        $resourceMock = $this->createMock(Resource::class);
        $resourceMock
            ->method('getLine')
            ->will($this->onConsecutiveCalls('some data', false)); // break the loop   

        // Refactoring getResource to constructor injection
        // would also get rid of the need to use a mock for FooClass here.
        // We could then do: $fooClass = new FooClass($resourceMock);
        $fooClass = $this->createMock(FooClass::class);
        $fooClass
            ->expects($this->any())
            ->method('getResource');
            ->willReturn($resourceMock);

        $this->assertNotEmpty($fooClass->barInterfaceMethod());
    }
}
Run Code Online (Sandbox Code Playgroud)

对于实际的Resource类,您不需要对那些围绕本机函数进行包装的方法进行单元测试。例子:

class Resource
{
    // We don't need to unit test this, all it does is call a PHP function
    public function write($data)
    {
        fwrite($this->handle, $data);
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,如果您想保持代码原样,另一种选择是设置您实际连接以用于测试目的的测试夹具资源。就像是

fsockopen($some_test_host, $port);
fopen($some_test_data_file);
// etc.
Run Code Online (Sandbox Code Playgroud)

Where$some_test_host包含一些您可能会混淆以进行测试的数据。


Tim*_*rib 3

资源模拟需要一些魔法。

大多数 I/O 函数允许使用协议包装器vfsStream使用此功能来模拟文件系统。不幸的是网络功能等不支持它fsockopen()

但您可以覆盖该函数。我不考虑RunkitAPD,你可以在没有 PHP 扩展的情况下做到这一点。

您在当前命名空间中创建一个具有相同名称和自定义实现的函数。另一个答案中描述了此技术。

还有《走吧!》在大多数情况下, AOP允许覆盖任何 PHP 函数。Codeception/AspectMock使用此功能来模拟功能。Badoo SoftMocks也提供此功能。

在您的示例中,您可以fsockopen()通过任何方法完全覆盖并使用 vfsStream 来调用fgets()fclose()和其他 I/O 函数。如果您不想测试该openConnection()方法,您可以模拟它来设置 vfsStream 并返回简单的文件资源。