I had a method, that opened a socket connection, used, and then closed it. In order to make it testable, I moved the dealing with the connection to separate methods (see the code below).
Now I want to write a unit test for the barIntrefaceMethod()
and need to mock the method openConnection()
. With other words, I need a fake resource
.
Is it possible / How to "manually" create a variable of the type resource
in PHP (in order to fake handles like "opened files, database connections, image canvas areas and the like" etc.)?
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);
}
}
As per my comments, I think you would be better off refactoring your code a little to remove the dependency on native functions being actually called with an actual resource handle. All you care about for the test of the FooClass::barInterfaceMethod
is that it returns a response. That class need not concern itself with whether the resource is opened, closed, written to, etc. So that is what should be mocked out.
Below I have rewritten what you have in your question to demonstrate, in some simplistic and non-production pseudo-code:
Your real class:
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;
}
}
Your test of this class:
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());
}
}
For the actual Resource
class, you don't need to then unit test those methods that are wrappers around native functions. Example:
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);
}
}
Lastly, the other alternative if you want to keep your code as-is is to setup test fixture resources that you actually connect to for test purposes. Something like
fsockopen($some_test_host, $port);
fopen($some_test_data_file);
// etc.
Where $some_test_host
contains some data you can mess with for tests.
Resource mocking requires some magic.
Most of I/O functions allows to use a protocol wrappers. This feature used by vfsStream to mock the file system. Unfortunately the network functions such as fsockopen()
don't support it.
But you can to override the function. I don't consider the Runkit and APD, you can do it without PHP extensions.
You create a function with same name and custom implementation in the current namespace. This technique is described in another answer.
Also the Go! AOP allows to override any PHP function in most cases. Codeception/AspectMock uses this feature to functions mocking. The Badoo SoftMocks also provides this functionality.
In the your example you can completely override fsockopen()
by any method and use vfsStream to call the fgets()
, fclose()
and other I/O functions.
If you don't want to test the openConnection()
method you can mock it to setup the vfsStream and return the simple file resource.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With