14

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.

hakre
  • 193,403
  • 52
  • 435
  • 836
Kevin Herrera
  • 2,941
  • 3
  • 19
  • 13
  • 3
    Great question. A good command line script shouldn't write "error" messages to stdout, but most PHP scripts do it anyway, probably because the language makes it less obvious how to do it (and also doesn't have output buffering for stderr like stdout has). – Mark E. Haase Dec 20 '11 at 22:09

3 Answers3

9

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.

David Harkness
  • 35,992
  • 10
  • 112
  • 134
  • 1
    Dammit. We answered at the same time. Your more classic approach (why didn't I think of that.. streams are to fun to play with `;)`) is fine. Maybe get rid of the foreach in the loop (logic in tests) but it's solid. +1 – edorian Dec 02 '11 at 00:00
7

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 :)

Community
  • 1
  • 1
edorian
  • 38,542
  • 15
  • 125
  • 143
  • 1
    I do like the use of streams, and you could easily encapsulate the memory-buffering and verification into a test helper class. You could add `setStream()` to allow injection and default to `STDERR` to avoid the reflection, but this shows what you'd need to do if you have less control over the class. Nice +1 – David Harkness Dec 02 '11 at 00:39
  • I'm not a big fan of setter injection in general (mostly I'd use it for refactoring legacy) and in this specific case I'd not touch the original class functionally wise. As both our solutions did. But you brought up an idea.. *editing* :) – edorian Dec 02 '11 at 00:54
  • 1
    Here's where a concrete example would help. For our logging class, I never bothered to write unit tests for its single-line methods. They will obviously fail during development and never be touched again. It's busy work that servers no benefit, and the test methods are just as complicated as the methods being tested. If I want to test that a class is logging appropriately, I mock the `Logger` itself rather than its output stream. – David Harkness Dec 02 '11 at 06:57
2

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();
rwb
  • 538
  • 2
  • 5
  • Good soln but a couple of minor things; rm `strtoupper()`, and probably don't want to append the output and have it echo twice. – spinkus Jun 26 '14 at 02:09