Reputation: 5028
I'm trying to reduce the dependencies of my program and make it more easily testable. One instance where I did this is in the __construct()
method of one of my classes. Before, it used to take in a file name and then the __construct()
method would use file_get_contents()
on that filename to save the contents into a property:
public function __construct($name){
$this->name = $name;
$this->contents = file_get_contents($name);
}
To reduce dependency on the filesystem I replaced this with:
public function __construct(SplFileObject $file){
$this->name = $file->getFilename();
$this->contents = '';
while(!$file->eof()){
$this->contents .= $file->fgets();
}
}
I believe that this is more easily testable, since I can mock up an SplFileObject
(which could be set to contain whatever content I want) and pass it in. The examples I have seen so far involve doing something like this:
$stub = $this->getMock('SplFileObject');
$stub->expects($this->any())
->method('fgets')
->will($this->returnValue('contents of file'));
However the mock fgets
method of the SplFileObject
will need to be more complicated - it needs to loop through each line of the contents, and stop when it has reached the end.
For the time being I have a solution that works - I just created an entirely new class called MockSplFileObject
which overrides these methods:
class MockSplFileObject extends SplFileObject{
public $maxLines;
public $filename;
public $contents;
public $currentLine = 1;
public function __construct($filename, $contents){
$this->filename = $filename;
$this->contents = explode("\n",$contents);
return true;
}
public function eof(){
if($this->currentLine == count($this->contents)+1){
return true;
}
return false;
}
public function fgets(){
$line = $this->contents[$this->currentLine-1];
$this->currentLine++;
return $line."\n";
}
public function getFilename(){
return $this->filename;
}
}
I then use this instead of calling PHPUnit's getMock()
function. My question is: is this a legitimate way of doing things? Or is there a better way of mocking up more complex methods?
Upvotes: 3
Views: 2421
Reputation: 43700
Use the onConsecutiveCalls()
method in your mock and return multiple lines for the file. You would be able to do the same thing for the eof()
. Your stub would look like this:
$stub = $this->getMock('SplFileObject');
$stub->expects($this->any())
->method('fgets')
->will($this->onConsecutiveCalls('line 1', 'line 2'));
$stub->expects($this->exactly(3))
->method('eof')
->will($this->onConsecutiveCalls(false, false, true));
Unfortunately the method doesn't take an array for the argument, so you can't pass in an array of the values to deal with. You could get around this by using returnCallback
and specify an array of data.
$calls = 0;
$contents = ['line 1', 'line 2'];
$stub = $this->getMock('SplFileObject');
$stub->expects($this->exactly(count($contents))
->method('fgets')
->will($this->returnCallback(function() use (&$calls, $contents)){
return $contents[$calls++];
});
$stub->expects($this->exactly(count($contents) + 1))
->method('eof')
->will($this->returnCallback(function() use ($calls, $contents){
if($calls <= count($contents)) {
return false;
} else {
return true;
}
});
With this method, you can add more data and the return is a little more flexible. You can add more lines to the "contents" without needing to remember to add an extra call for the EOF check.
Upvotes: 3
Reputation: 4839
$fileObject = $this->getMock('SplFileObject', [], ['php://memory']);
$fileObject
->expects($this->any())
->method('fgets')
->will($this->onConsecutiveCalls('line 1', 'line 2'));
$fileObject
->expects($this->exactly(3))
->method('eof')
->will($this->onConsecutiveCalls(false, false, true));
Using 'php://memory'
as an argument to the SplFileObject helped me avoid the following error which surfaces when you try to mock SplFileObject
PHP Fatal error: Uncaught exception 'LogicException' with message 'The parent constructor was not called: the object is in an invalid state'
Upvotes: 6
Reputation: 41
What you try to do is stubing internal function. Complexity of method doesn't have much to the problem. First solution is to throw away responsibility of reading file. Your class needs contents only and some name so deeper knowledge about file is not really needed (assumption). If any memory issues are not considered then i would use simple DTO object (simple object with getters and setters only) with name and contents. I assume that your class is not responsible for reading file... Then you can simply put the filled DTO object as dependency in constructor without any concern. Your solution needs the file mock to be unit tested as the normal Domain Class...
Second solution is to extract file_get_contents into method like
public function __construct($name){
$this->name = $name;
$this->contents = $this->getFileContents($name);
}
private function getFileContents($fileFullPath) {
return file_get_contents($fileFullPath);
}
Then you can stub this function in mock and test the mock. This solution apply when you would like to stub some global state or static code.
I would prefer first solution unless your class is responsible for reading file...
Hope helpful
Upvotes: 3