我有一个方法,打开套接字连接,使用,然后关闭它。为了使其可测试,我将连接处理移至单独的方法(参见下面的代码)。
现在我想为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);
}
}
答案 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 wrappers。 vfsStream用于模拟文件系统的此功能。遗憾的是,fsockopen()
等网络功能不支持。
但你可以覆盖这个功能。我不考虑Runkit和APD,你可以在没有PHP扩展的情况下进行。
您在当前命名空间中创建具有相同名称和自定义实现的函数。另一种answer中描述了这种技术。
在大多数情况下,Go! AOP允许override任何PHP函数。 Codeception/AspectMock将此功能用于functions mocking。 Badoo SoftMocks也提供此功能。
在您的示例中,您可以通过任何方法完全覆盖fsockopen()
,并使用vfsStream来调用fgets()
,fclose()
和其他I / O函数。
如果您不想测试openConnection()
方法,可以模拟它以设置vfsStream并返回简单文件资源。