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

有没有办法在PHPUnit中测试STDERR输出?,php,phpunit,Php,Phpunit,我有一个类输出到STDERR,但是我很难找到让PHPUnit测试其输出的方法 类,PHPUnit\u Extensions\u OutputTestCase,也不起作用。我看不到一种方法可以像使用stdout那样缓冲stderr,因此我会重构您的类,将实际输出的调用移动到一个新方法。这将允许您在测试期间模拟该方法,以使用缓冲区验证输出或子类 例如,假设有一个类列出目录中的文件 class DirLister { public function list($path) {

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


类,
PHPUnit\u Extensions\u OutputTestCase
,也不起作用。

我看不到一种方法可以像使用
stdout
那样缓冲
stderr
,因此我会重构您的类,将实际输出的调用移动到一个新方法。这将允许您在测试期间模拟该方法,以使用缓冲区验证输出或子类

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

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 ;
    }
}
第二,在测试中模拟或子类化它。如果您有一个简单的测试,或者不希望对
输出调用太多,那么模拟就很容易了。如果要验证大量输出,则子类化以缓冲输出会更容易

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

覆盖
输出
以将所有
$text
缓冲到内部缓冲区是留给读者的练习。

您不能截取和
写入(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");
    }

}

更新 在考虑过之后,我想说,没有像
errorSteamWriter
这样的类也可以

只需拥有一个
StreamWriter
,并使用
新StreamWriter(STDERR)构建它
将产生一个很好的可测试类,该类可以在应用程序中为许多目的重用,而无需在类本身中硬编码某种类型的“这就是错误所在”,并增加灵活性


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

使用流过滤器捕获/重定向输出到STDERR。下面的示例实际上对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();

该死。我们同时回答。您的更经典的方法(为什么我没有想到这一点?..streams玩起来很有趣)
)很好。也许可以去掉循环中的foreach(测试中的逻辑),但它是可靠的+1我确实喜欢使用流,您可以轻松地将内存缓冲和验证封装到测试助手类中。您可以添加
setStream()
以允许注入,并将默认值添加到
STDERR
以避免反射,但这显示了如果您对类的控制较少,您需要做什么。Nice+1一般来说,我不太喜欢setter注入(主要是在重构遗留类时使用),在这种特定情况下,我不会从功能上接触原始类。正如我们两个解决方案所做的那样。但是你提出了一个想法。。编辑:)下面是一个具体的例子。对于我们的日志类,我从来没有为它的单行方法编写过单元测试。它们在开发过程中显然会失败,永远不会再被触及。这是一项繁忙的工作,服务器没有任何好处,而且测试方法和正在测试的方法一样复杂。如果我想测试一个类是否正确地记录日志,我会模拟
记录器本身,而不是它的输出流。一个好的命令行脚本不应该向stdout写入“错误”消息,但大多数PHP脚本无论如何都会这样做,可能是因为该语言不太清楚如何去做(也不像stdout那样为stderr提供输出缓冲);rm
strtoupper()
,并且可能不希望附加输出并使其回显两次。
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();