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

如何在PHPUnit中存根更复杂的方法,php,unit-testing,testing,phpunit,Php,Unit Testing,Testing,Phpunit,我试图减少我的程序的依赖性,使它更容易测试。我这样做的一个实例是在我的一个类的\uu construct()方法中。以前,它通常接收文件名,然后\u construct()方法将在该文件名上使用file\u get\u contents()将内容保存到属性中: public function __construct($name){ $this->name = $name; $this->contents = file_get_contents($name); } 为了减少对

我试图减少我的程序的依赖性,使它更容易测试。我这样做的一个实例是在我的一个类的
\uu construct()
方法中。以前,它通常接收文件名,然后
\u construct()
方法将在该文件名上使用
file\u get\u 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'));
$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));
但是,
SplFileObject
的mock
fgets
方法需要更加复杂-它需要循环遍历每一行内容,并在到达末尾时停止

目前我有一个可行的解决方案——我刚刚创建了一个名为
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()
函数。我的问题是:这是一种合法的做事方式吗?或者有没有更好的方法来模拟更复杂的方法?

您试图做的是删除内部函数。方法的复杂性与问题无关。 第一个解决方案是丢掉读取文件的责任。您的类只需要内容和一些名称,所以不需要对文件有更深入的了解(假设)。如果没有考虑任何内存问题,那么我将使用名称和内容的简单DTO对象(仅包含getter和setter的简单对象)。我假设你们班不负责阅读文件。。。然后,您可以简单地将填充的DTO对象作为依赖项放入构造函数中,而不必担心。您的解决方案需要将文件mock作为普通域类进行单元测试

第二种解决方案是将文件\u get\u内容提取到如下方法中

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

private function getFileContents($fileFullPath) {
    return file_get_contents($fileFullPath);
}
然后您可以在mock中存根这个函数并测试mock。当您想要存根某些全局状态或静态代码时,此解决方案适用

我更喜欢第一种解决方案,除非你们班负责阅读文件

希望对您有所帮助

在模拟中使用该方法并为文件返回多行。您可以对
eof()
执行相同的操作。您的存根如下所示:

$stub = $this->getMock('SplFileObject');
$stub->expects($this->any())
     ->method('fgets')
     ->will($this->returnValue('contents of file'));
$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));
不幸的是,该方法并没有将数组作为参数,所以您无法传入要处理的值数组。您可以通过使用并指定一个数据数组来解决这个问题

$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;
    }
});
$calls=0;
$contents=['第1行','第2行'];
$stub=$this->getMock('SplFileObject');
$stub->expected($this->justice(count($contents))
->方法('fgets')
->will($this->returnCallback(函数()使用(&$calls,$contents)){
返回$contents[$calls++];
});
$stub->expected($this->justice(计数($contents)+1))
->方法('eof')
->将($this->returnCallback(函数()使用($calls,$contents){
如果打电话
使用
'php://memory“
作为SplFileObject的一个参数,帮助我避免了在尝试模拟SplFileObject时出现的以下错误

PHP致命错误:未捕获异常“LogicException”,消息为“未调用父构造函数:对象处于无效状态”

由于getMock()已被弃用,请使用:
$this->getMockBuilder('SplFileObject')->setConstructorArgs(['php://memory']->getMock();