Kevin Herrera
Kevin Herrera

Reputation: 2951

Is there a way test STDERR output in PHPUnit?

I have a class that outputs to STDERR, but I am having trouble finding a way to get PHPUnit to test its output.

The class, PHPUnit_Extensions_OutputTestCase, also did not work.

Upvotes: 14

Views: 6749

Answers (3)

David Harkness
David Harkness

Reputation: 36532

I don't see a way to buffer stderr as you can with stdout, so I would refactor your class to move the call that does the actual output to a new method. This will allow you to mock that method during testing to validate the output or subclass with one that buffers.

For example, say you have a class that lists files in a directory.

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

First, extract the call to echo. Make it protected so you can override and/or mock it.

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

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

Second, either mock or subclass it in your test. Mocking is easy if you have a simple test or don't expect many calls to output. Subclassing to buffer the output is easier if you have a large amount of output to verify.

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

Overriding output to buffer all $text into an internal buffer is left as an exercise for the reader.

Upvotes: 9

rwb
rwb

Reputation: 558

Use a stream filter to capture/redirect the output to STDERR. The following example actually upercases and redirects to STDOUT but conveys the basic idea.

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();

Upvotes: 2

edorian
edorian

Reputation: 38961

You can't intercept and fwrite(STDERR); from within a test case with the help of phpunit. For that matter you can't even intercept fwrite(STDOUT);, not even with output buffering.

Since i assume you don't really want to inject STDERR into your "errorOutputWriter" (as it doesn't make any sense for the class to write somewhere else) this is one of the very few cases where I'd suggest that sort of small hack:

<?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");
    }

}

It takes out the stderr stream and replaces it with an in memory stream and read that one back in the testcase to see if the right output was written.

Normally I'd for sure say "Inject the file path in the class" but with STDERR that doesn't make any sense to me so that would be my solution.

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

.

Time: 0 seconds, Memory: 5.00Mb

OK (1 test, 1 assertion)

Update

After giving it some thought I'd say that not having somethink like an errorSteamWriter as a class might also work out.

Just having a StreamWriter and constructing it with new StreamWriter(STDERR); would result in a nicely testable class that can be reused for a lot of purposes in the application without hard coding some sort of "this is where errors go" into the class it's self and adding flexibility.

Just wanted to add this as an option to avoid the "ugly" test options :)

Upvotes: 7

Related Questions