如何在PHPUnit中存根更复杂的方法

时间:2014-04-14 14:04:40

标签: php unit-testing testing phpunit

我试图减少程序的依赖性并使其更容易测试。我执行此操作的一个实例是我的一个类的__construct()方法。之前,它曾经接受文件名,然后__construct()方法将使用该文件名file_get_contents()将内容保存到属性中:

public function __construct($name){
  $this->name = $name;
  $this->contents = file_get_contents($name);
}

为了减少对文件系统的依赖,我将其替换为:

public function __construct(SplFileObject $file){
  $this->name = $file->getFilename();
  $this->contents = '';
  while(!$file->eof()){
    $this->contents .= $file->fgets();
  }
}

我相信这更容易测试,因为我可以模拟一个SplFileObject(可以设置为包含我想要的任何内容)并传递它。我到目前为止看到的例子涉及做某事像这样:

$stub = $this->getMock('SplFileObject');
$stub->expects($this->any())
     ->method('fgets')
     ->will($this->returnValue('contents of file'));

然而fgets的模拟SplFileObject方法需要更复杂 - 它需要循环遍历内容的每一行,并在它到达结束时停止。

暂时我有一个有效的解决方案 - 我刚刚创建了一个名为MockSplFileObject的全新类,它覆盖了这些方法:

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;
  }
}

然后我使用它而不是调用PHPUnit的getMock()函数。我的问题是:这是一种合法的做事方式吗?或者有更好的方法来模拟更复杂的方法吗?

3 个答案:

答案 0 :(得分:5)

$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));

使用'php://memory'作为SplFileObject的参数帮助我避免了当您尝试模拟SplFileObject时出现的以下错误

PHP Fatal error: Uncaught exception 'LogicException' with message 'The parent constructor was not called: the object is in an invalid state'

答案 1 :(得分:3)

在模拟中使用onConsecutiveCalls()方法,并为文件返回多行。你可以为eof()做同样的事情。你的存根看起来像这样:

$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));

不幸的是,该方法没有为参数采用数组,因此您无法传入要处理的值数组。您可以使用returnCallback来解决这个问题,并指定数据数组。

$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;
    }
});

使用此方法,您可以添加更多数据,并且返回更灵活一些。您可以在“内容”中添加更多行,而无需记住为EOF检查添加额外的调用。

答案 2 :(得分:2)

您尝试做的是对内部函数进行存根。方法的复杂性对问题没有太大影响。 第一个解决方案是抛弃阅读文件的责任。你的类只需要内容和一些名称,因此不需要更深入的文件知识(假设)。如果没有考虑任何内存问题,那么我将使用简单的DTO对象(仅带有getter和setter的简单对象)和名称和内容。我假设您的类不负责读取文件...然后您可以简单地将填充的DTO对象作为依赖项放在构造函数中而不用担心。您的解决方案需要将模拟文件作为正常的域类进行单元测试...

第二个解决方案是将file_get_contents提取到类似

的方法中
public function __construct($name){
    $this->name = $name;
    $this->contents = $this->getFileContents($name);
}

private function getFileContents($fileFullPath) {
    return file_get_contents($fileFullPath);
}

然后你可以在mock中存根这个函数并测试mock。当您想要存根某些全局状态或静态代码时,此解决方案适用。

除非您的班级负责阅读文件,否则我更倾向于第一个解决方案......

希望有用