【问题标题】:How to fake a resourse for a unit test in PHP?如何在 PHP 中为单元测试伪造资源?
【发布时间】:2016-12-23 11:41:49
【问题描述】:

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

现在我想为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);
    }
}

【问题讨论】:

  • 我不确定您是否以正确的方式处理此问题。如果我对您的理解正确,您实际上是想针对从$resource->openConnection() 返回的模拟/伪造资源对象测试 PHP 内置方法(fwrite、feof 等)。您应该考虑在这些方法周围使用一个包装器对象,以便您可以测试 $resource->write('something') 而不是 fwrite($resource, 'something')
  • 感谢您的评论!不,当然,我不会不测试本机功能。而且,是的,我已经创建了包装连接设置相关 PHP 函数的方法。但是现在我想模拟他们的结果来测试barIntrefaceMethod(),它调用了这些包装函数。问题只是,我找不到任何方法来伪造resource。这使嘲笑openConnection() 感到沮丧。
  • 我知道您不会测试本机函数,但您必须在代码中调用它们并且它们依赖于真实的资源句柄。最简单且设计更好的方法是让一个对象包裹在本机函数周围,然后模拟 that 。我将发布一个答案以更好地解释我在代码中的意思。

标签: php unit-testing mocking phpunit fsockopen


【解决方案1】:

根据我的 cmets,我认为您最好稍微重构一下代码以消除对使用实际资源句柄实际调用的本机函数的依赖。对于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 包含一些您可以用来进行测试的数据。

【讨论】:

  • 为什么不$this->assertSame("some data", $fooClass->barInterfaceMethod());我猜是不是更好?
【解决方案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 并返回简单文件资源。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-11
    • 2021-07-23
    • 1970-01-01
    • 2021-07-08
    相关资源
    最近更新 更多