【问题标题】:How to stub more complicated methods in PHPUnit如何在 PHPUnit 中存根更复杂的方法
【发布时间】:2014-04-14 14:04:39
【问题描述】:

我正在尝试减少程序的依赖性并使其更易于测试。我这样做的一个例子是在我的一个类的__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() 函数。我的问题是:这是一种合法的做事方式吗?或者有没有更好的方法来模拟更复杂的方法?

【问题讨论】:

    标签: php unit-testing testing phpunit


    【解决方案1】:
    $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'

    【讨论】:

    • 由于 getMock() 已被弃用,请使用:$this->getMockBuilder('SplFileObject')->setConstructorArgs(['php://memory'])->getMock();
    【解决方案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。当您想要存根一些全局状态或静态代码时,此解决方案适用。

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

    希望有帮助

    【讨论】:

      【解决方案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 检查添加额外调用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-01-03
        • 1970-01-01
        • 2014-09-09
        • 2011-03-06
        • 1970-01-01
        • 2015-04-09
        • 2021-11-08
        • 2014-09-04
        相关资源
        最近更新 更多