user1578653
user1578653

Reputation: 5028

How to stub more complicated methods in PHPUnit

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

Answers (3)

Schleis
Schleis

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

nackjicholson
nackjicholson

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

CezaryDraus
CezaryDraus

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

Related Questions