Having mastered the ability to cause time to stand still, the next challenge I faced while writing what are probably technically not unit tests, was a lack of direction, or rather too much direction.

Consider something like this:

class Some_Class {

	function __construct() {
		add_action( 'template_redirect', [ $this, 'access_handler' ] );
	}

	function access_handler() {

		if ( ! current_user_can( 'do_stuff_on_this_page' ) ) {
			wp_safe_redirect( get_site_url() );
			exit;
		}

		return true;
	}

	...
}

So far so good, if the user has the correct capabilities they can view the page. If they don’t, they get redirected to the home page. Lets write a simple test next:

function test_access_handler() {

	// Create a page.
	$page_id = $this->factory->post->create( [ 'post_type' => 'page' ] );

	// Go to it.
	$this->go_to( get_permalink( $page_id ) );

	$some_class = new Some_Class();

	// Test our function.
	$this->assertTrue( true !== $some_class->access_handler() );
}

But what’s this madness? I run my unit tests like the diligent coder I am, and this is what I get back:

Cannot modify header information - headers already sent by (output started at /srv/www/wordpress-develop/tests/phpunit/includes/bootstrap.php:59)

Luckily, there is an answer (it just took me several answers to get to the right one)…

Initially I thought that the answer was using set_error_handler to define expected errors.

protected function expected_errors( $error_messages ) {

	$this->expected_error_list = (array) $error_messages;
	set_error_handler( [ &$this, 'expected_errors_handler' ] );
}

public function expected_errors_handler( $errno, $errstr ) {

	foreach ( $this->expected_error_list as $expect ) {

		if ( strpos( $errstr, $expect ) !== false ) {
			$this->expected_errors_found = true;
			return true;
		}
	}

	return false;
}

function test_access_handler() {

	// Establish the error handler with the expected message.
	$expected_error = 'Cannot modify header information';
	$this->expected_errors( $expected_error );

	// Create a page.
	$page_id = $this->factory->post->create( [ 'post_type' => 'page' ] );

	// Go to it.
	$this->go_to( get_permalink( $page_id ) );

	$some_class = new Some_class();

	// Test our function.
	$this->assertTrue( true !== $some_class->access_handler() );
}

However, all that happens in this case is that the test calls the access_handler method, suppresses the error and then hits the exit call and grinds to an unimpressive halt. To demonstrate this, use the example above and add the following at the end:

$this->assertEquals( 1, 2 );

You won’t get a fail for the incorrect assertion that 1 is equal to 2.

Some frantic googling later, and I came across another solution. Move the redirect and exit call to a separate method and use Mocks to overwrite it when testing.

class Some_Class {

	function __construct() {
		add_action( 'template_redirect', [ $this, 'access_handler' ] );
	}

	function access_handler() {

		if ( ! current_user_can( 'do_stuff_on_this_page' ) ) {
			return $this->call_redirect();
		}

		return true;
	}

	protected function call_redirect() {
		wp_safe_redirect( get_site_url() );
		exit;
	}

	...
}

Our test would then look something like this:

function test_access_handler() {

	$some_class = $this->getMockBuilder( 'Some_Class' )
		->setMethods( [ 'call_redirect' ] )
		->getMock();

	$some_class->expects( $this->once() )
		->method( 'call_redirect' );

	// Create a page.
	$page_id = $this->factory->post->create( [ 'post_type' => 'page' ] );

	// Go to it.
	$this->go_to( get_permalink( $page_id ) );

	$this->assertTrue( true !== $some_class->access_handler() );
}

This works fine, and ensures that all tests run to completion with no interruption. Note that the test above will return two assertions (one being the assertTrue for the response, and the other as a result of the expects() method), but it still feels like a bit of a hack.

Mocks are great for replicating what a call to a third party API might return so that you can run consistent tests, but this seems like a bit of a quick and easy fix. What if we extend our access_handler method to include another ten checks for user role, capability or general permissions? Do we really want to create Mocks for every single test?

Luckily the coffee must have been working well because I had a eureka moment (or “realised the bleedin’ obvious” in “non-hack developer” terminology).

If we leave the access_handler method as a controller of sorts, we can call a separate method from within it that determines whether or not to redirect. This method can simply return a boolean value (easy to test for), and the controller can decide what to do from there.

class Some_Class {

	function __construct() {
		add_action( 'template_redirect', [ $this, 'access_handler' ] );
	}

	function access_handler() {

		if ( ! $this->access_handler_checks() ) {
			return $this->call_redirect();
		}

		return true;
	}

	function access_handler_checks() {

		if ( ! current_user_can( 'do_stuff_on_this_page' ) ) {
			return false;
		}

		// Any further role / capability / permissions tests.

		return true;
	}

	protected function call_redirect() {
		wp_safe_redirect( get_site_url() );
		exit;
	}

	...
}

// Tests.

// If you want to test that the redirect method is being called, you can keep this in place.
function test_access_handler() {

	$some_class = $this->getMockBuilder( 'Some_Class' )
		->setMethods( [ 'call_redirect' ] )
		->getMock();

	$some_class->expects( $this->once() )
		->method( 'call_redirect' );

	// Create a page.
	$page_id = $this->factory->post->create( [ 'post_type' => 'page' ] );

	// Go to it.
	$this->go_to( get_permalink( $page_id ) );
	$this->assertTrue( true !== $some_class->access_handler() );
}

// Test all your intricate permissions here without catching errors, or using Mocks.
function test_access_handler_checks() {

	// Create a user and log them in.
	$user = $this->factory->user->create( [ 
		'role' => 'some_role_without_access_rights'
	] );

	wp_set_current_user( $user );

	// Create a page.
	$page_id = $this->factory->post->create( [ 'post_type' => 'page' ] );

	// Go to it.
	$this->go_to( get_permalink( $page_id ) );

	// Test.
	$some_class = new Some_class();
	$this->assertFalse( $some_class->access_handler_checks() );

	// Create a user with permissions and log them in.
	$user = $this->factory->user->create( [ 
		'role' => 'some_role_with_access_rights'
	] );

	wp_set_current_user( $user );

	// Go to page.
	$this->go_to( get_permalink( $page_id ) );

	// Test.
	$some_class = new Some_class();
	$this->assertTrue( $some_class->access_handler_checks() );
}

We can still write a test using our Mock to be sure that the controller is passing the user off to the redirect method where required, but now we can write simple unit tests for our access_handler_checks method to ascertain whether the correct users are being allowed access to the correct pages.

Advertisements

4 thoughts on “Testing methods that redirect with PHPUnit

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s