如何在PHPUnit中对更复杂的方法进行存根测试

3

我试图减少程序的依赖性并使其更易于测试。我在一个类的__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'));

然而,SplFileObject的模拟方法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()函数。我的问题是:这种做法合理吗?或者有更好的模拟更复杂方法的方式吗?

3个回答

6
$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'

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

3
你正在尝试存根内部函数。方法的复杂性对问题没有太大影响。第一个解决方案是放弃读取文件的责任。你的类只需要内容和一些名称,因此不需要更深入地了解文件(假设)。如果不考虑任何内存问题,则可以使用简单的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);
}

然后你可以在模拟和测试模拟中存根此函数。当您想要存根一些全局状态或静态代码时,可以使用此解决方案。

除非您的类负责读取文件,否则我更喜欢第一种解决方案...

希望有所帮助


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检查调用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接