问题 有没有办法在PHPUnit中测试STDERR输出?


我有一个输出到的类 STDERR,但我找不到让PHPUnit测试其输出的方法。

班上, PHPUnit_Extensions_OutputTestCase,也没用。


8507
2017-12-01 22:03


起源

好问题。一个好的命令行脚本不应该向stdout写入“错误”消息,但大多数PHP脚本无论如何都会这样做,可能是因为该语言使得它不那么明显(并且没有像stdout这样的stderr的输出缓冲)。 - Mark E. Haase


答案:


我没有看到缓冲的方法 stderr 尽你所能 stdout,所以我会重构你的类来将执行输出的调用移动到新方法。这将允许您在测试期间模拟该方法,以使用缓冲区验证输出或子类。

例如,假设您有一个列出目录中文件的类。

class DirLister {
    public function list($path) {
        foreach (scandir($path) as $file) {
            echo $file . "\n";
        }
    }
}

首先,提取呼叫 echo。使其受到保护,以便您可以覆盖和/或模拟它。

class DirLister {
    public function list($path) {
        foreach (scandir($path) as $file) {
            $this->output($file . "\n");
        }
    }

    protected function output($text) {
        echo $text ;
    }
}

其次,在测试中模拟或子类化。如果您进行简单的测试或者不期望多次调用,那么模拟很容易 output。如果要验证大量输出,则子类化缓冲输出会更容易。

class DirListTest extends PHPUnit_Framework_TestCase {
    public function testHomeDir() {
        $list = $this->getMock('DirList', array('output'));
        $list->expects($this->at(0))->method('output')->with("a\n");
        $list->expects($this->at(1))->method('output')->with("b\n");
        $list->expects($this->at(2))->method('output')->with("c\n");
        $list->list('_files/DirList'); // contains files 'a', 'b', and 'c'
    }
}

重写 output 缓冲所有 $text 进入内部缓冲区留给读者练习。


8
2017-12-01 23:52



该死。我们同时回答。你更经典的方法(为什么我没想到......流是很有趣的玩法 ;))很好。也许摆脱循环中的foreach(测试中的逻辑),但它是可靠的。 +1 - edorian
+1很好的解释。 - hakre


你不能拦截和 fwrite(STDERR); 在phpunit的帮助下从测试用例中。就此而言,你甚至无法拦截 fwrite(STDOUT);,甚至没有输出缓冲。

因为我假设你真的不想注射 STDERR 进入你的“errorOutputWriter“(因为这个类在其他地方写的没有任何意义)这是我建议那种小黑客的极少数情况之一:

<?php 

class errorStreamWriterTest extends PHPUnit_Framework_TestCase {

    public function setUp() {
        $this->writer = new errorStreamWriter();
        $streamProp = new ReflectionProperty($this->writer, 'errorStream');
        $this->stream = fopen('php://memory', 'rw');
        $streamProp->setAccessible(true);
        $streamProp->setValue($this->writer, $this->stream);
    }

    public function testLog() {
        $this->writer->log("myMessage");
        fseek($this->stream, 0);
        $this->assertSame(
            "Error: myMessage",
            stream_get_contents($this->stream)
        );
    }

}

/* Original writer*/
class errorStreamWriter {
    public function log($message) {
        fwrite(STDERR, "Error: $message");
    }
}

// New writer:
class errorStreamWriter {

    protected $errorStream = STDERR;

    public function log($message) {
        fwrite($this->errorStream, "Error: $message");
    }

}

它取出stderr流并将其替换为内存流,并在测试用例中读回一个以查看是否写入了正确的输出。

通常,我肯定会说“在类中注入文件路径”但是 STDERR 这对我没有任何意义,所以这将是我的解决方案。

phpunit stderrTest.php 
PHPUnit @package_version@ by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.00Mb

OK (1 test, 1 assertion)

更新

在给它一些想法后,我会说没有像想象的那样 errorSteamWriter 作为一个班级也可能会成功。

只是有一个 StreamWriter 并用它构建它 new StreamWriter(STDERR); 将导致一个可测试的类很好,可以在应用程序中重复用于很多目的而无需硬编码某种“这就是错误发生在它自己的类中”并增加了灵活性。

只是想添加这个作为一个选项,以避免“丑陋”的测试选项:)


6
2017-12-01 23:55



我喜欢使用流,你可以很容易地将内存缓冲和验证封装到测试助手类中。你可以添加 setStream()允许注射和默认 STDERR 避免反射,但这表明如果你对类的控制较少,你需要做什么。尼斯+1 - David Harkness
一般来说,我不太喜欢二传手注射(大多数情况下我会用它来重构传统),在这种特殊情况下,我不会在功能上明智地触及原始类。正如我们的解决方案一样。但你提出了一个想法...... 编辑 :) - edorian
这是一个具体的例子有用的地方。对于我们的日志类,我从不打算为其单行方法编写单元测试。它们在开发过程中显然会失败,永远不会再被触及。繁忙的工作对服务器没有好处,测试方法和测试方法一样复杂。如果我想测试一个类是否正确记录,我嘲笑 Logger 本身而不是其输出流。 - David Harkness


使用流过滤器捕获/重定向输出到STDERR。以下示例实际上是upercases并重定向到STDOUT,但传达了基本思想。

class RedirectFilter extends php_user_filter {
  static $redirect;

  function filter($in, $out, &$consumed, $closing) {
while ($bucket = stream_bucket_make_writeable($in)) {
  $bucket->data = strtoupper($bucket->data);
  $consumed += $bucket->datalen;
  stream_bucket_append($out, $bucket);
  fwrite(STDOUT, $bucket->data);
}
return PSFS_PASS_ON;
  }
}

stream_filter_register("redirect", "RedirectFilter")
or die("Failed to register filter");

function start_redirect() {
  RedirectFilter::$redirect = stream_filter_prepend(STDERR, "redirect", STREAM_FILTER_WRITE);
}

function stop_redirect() {
  stream_filter_remove( RedirectFilter::$redirect);
}

start_redirect();
fwrite(STDERR, "test 1\n");
stop_redirect();

2
2018-01-26 23:53



好的解决,但几件小事; R M strtoupper(),可能不想附加输出并让它回显两次。 - spinkus