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

时间:2016-12-23 11:41:49

标签: php unit-testing mocking phpunit fsockopen

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

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

是否可能/如何"手动"在PHP中创建一个resource类型的变量(为了伪造诸如" opened files, database connections, image canvas areas and the like"等句柄?)

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

2 个答案:

答案 0 :(得分:3)

根据我的评论,我认为你最好稍微重构你的代码以消除对实际资源句柄实际调用的本机函数的依赖。您对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;
    }
}

您对此课程的考试:

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

对于实际的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);
    }
}

最后,如果您希望保持代码原样,另一种方法是设置实际连接的测试夹具资源以进行测试。像

这样的东西
fsockopen($some_test_host, $port);
fopen($some_test_data_file);
// etc.

$some_test_host包含一些数据,你可以搞砸进行测试。

答案 1 :(得分:2)

资源嘲讽需要一些魔力。

大多数I / O功能允许使用protocol wrappersvfsStream用于模拟文件系统的此功能。遗憾的是,fsockopen() 等网络功能不支持

但你可以覆盖这个功能。我不考虑RunkitAPD,你可以在没有PHP扩展的情况下进行。

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

在大多数情况下,Go! AOP允许override任何PHP函数。 Codeception/AspectMock将此功能用于functions mockingBadoo SoftMocks也提供此功能。

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