2015-02-23

Unit testing headers in PHP

There's plenty of questions about unit testing code that sets headers using PHPUnit. PHPUnit officially assumes that neither the test code nor the tested code emit output or send headers:

(from https://github.com/sebastianbergmann/phpunit/issues/720#issuecomment-10399612)

And rightfully so. Headers are essentially global state, which is hard to unit test. However, if you're willing to install the PECL extension Runkit, you can actually unit test headers if you really want to.

<?php
/**
* Class that tries to send a header.
*/
class SendsHeaders {
/**
* Send a header to the user's browser.
*/
public function sendHeader() {
header('This is a header', true, 400);
}
}
/**
* Unit tests for SendHeaders class.
*/
class SendsHeadersTest extends PHPUnit_Framework_TestCase {
/**
* Array of headers that have been sent.
* @var array
*/
public static $headers;
/**
* Subject under test.
* @var SendsHeaders
*/
protected $sendsHeaders;
/**
* Use runkit to create a new header function.
*/
public static function setUpBeforeClass() {
if (!extension_loaded('runkit')) {
return;
}
// First backup the real header function so we can restore it.
runkit_function_rename('header', 'header_old');
// Now, create a new header function that makes things testable.
runkit_function_add(
'header',
'$string,$replace=true,$http_response_code=null',
'SendsHeadersTest::$headers[] = array($string,$replace,$http_response_code);'
);
}
/**
* After we're done testing, restore the header function.
*/
public static function tearDownAfterClass() {
if (!extension_loaded('runkit')) {
return;
}
// Get rid of our new header function.
runkit_function_remove('header');
// Move our backup to restore header to its original glory.
runkit_function_rename('header_old', 'header');
}
/**
* Set up our subject under test and global header state.
*/
protected function setUp() {
$this->sendsHeaders = new SendsHeaders();
self::$headers = array();
}
/**
* Unit test sending a header.
* @requires extension runkit
* @test
*/
public function testSendHeader() {
$expectedHeaders = array(
array('This is a header', true, 400),
);
$this->sendsHeaders->sendHeader();
$this->assertEquals($expectedHeaders, self::$headers);
}
}
view raw headers.php hosted with ❤ by GitHub
I use the setupBeforeClass hook to only run the code once before the entire class runs. If you only have one method that you're testing it can all get stuck in the single test. I check if the runkit extension is loaded to avoid breaking the unit test suite if someone tries to run it that doesn't have runkit installed. The test that actually looks at the headers is marked with a requires annotation, which will mark that test as skipped if runkit isn't loaded. If every test in your test class tests headers, you can move that up to the class level if you'd like. My particular class had some tests that didn't need runkit and I wanted those to still run for people that didn't have runkit loaded.

1 comment:

  1. I've had a similar issue in the past. If I recall the "official" answer from a PHPUnit bug ticket was to batch up your headers to send in an array/property/Response object, and then test the contents of that. It still relies on PHP knowing how to send a header correctly, but you don't open up the can of runkit worms. There are a few more helpful comments here too:

    http://www.brianfenton.us/2011/01/unit-testing-with-phpunit.html

    ReplyDelete