This third post in my series on testing
WordPress plugins with
PHPUnit
will cover the real world dilemma I ran into combining wp_logout()
,
wp_redirect()
, and continuing a given test's execution after PHP
produces expected errors.
When a PHP error/warning/whatever is encountered inside a test, PHPUnit
throws an exception, immediately stopping execution of that test. It shows the
problem in errors section of the results once all tests have completed. But
what if you want to test the fact
that PHP is supposed to produce an error? PHPUnit lets you do that with
the setExpectedException('PHPUnit_Framework_Error', 'some message')
method or with docblock annotations. It's great stuff.
But there's one big limitation. What I said about PHPUnit "immediately stopping execution of that test" still applies. Any code after the PHP error will not be run (and it's easy to never even realize it). Here's an example.
/** * @expectedException PHPUnit_Framework_Error * @expectedExceptionMessage Notice: Undefined variable: nada */ public function test_error() { echo "$nada\n"; // Execution stops above. The rest of this stuff will not be run. $actual = some_procedure(); $this->assertFalse($actual); }
That test will pass, even if there's a problem with the return from
some_procedure()
. Don't despair! This post explains how to deal
with the problem.
Some of the following concepts are similar to those discussed for
testing wp_mail()
.
Please bear any slight repetition.
First, a bit of background. In this post, when I say "parent class," I mean the class that holds helper properties and methods that all test classes can reference:
abstract class TestCase extends PHPUnit_Framework_TestCase {}
And when I say "test class," I mean the class containing the test methods that PHPUnit will execute:
class LoginTest extends TestCase {}
Monitoring wp_redirect()
itself is pretty easy. WordPress lets
users override the one built into the core. All users have to do is declare
it before WP does. My version calls a static method in the
"parent class."
function wp_redirect($location, $status = 302) { TestCase::wp_redirect($location, $status); }
The redirect method in the "parent class" writes the location to a
property for later comparison. But before doing that, it makes sure that
wp_redirect()
was called at an expected time by checking that the
$location_expected property has been set.
public static function wp_redirect($location, $status) { if (!self::$location_expected) { throw new Exception('wp_redirect() called at unexpected time' . ' ($location_expected was not set).'); } self::$location_actual = $location; }
Here's the plugin's method we'll be testing. It logs the user out and redirects them to the password reset page.
protected function force_retrieve_pw() { wp_logout(); wp_redirect(wp_login_url() . '?action=retrievepassword'); }
The hitch is wp_logout()
calls setcookie()
, which
sends headers, but PHPUnit has already generated some output, meaning (you
guessed it) PHP will produce a warning: Cannot modify header information
- headers already sent by (output started at
PHPUnit/Util/Printer.php:172). That means I won't be able to make sure
the redirect worked because PHPUnit will stop execution when that warning is
thrown.
Thought bubble... I just realized
wp_logout()
andwp_redirect()
are "pluggable" too. So I could override them with methods that don't set cookies and even makes sure the function gets called when I want it to. But that would obviate the need for the cool part of this tutorial. So, uh, let's imagine I never said anything. (Eventually I'll modify the unit tests in the Login Security Solution.)
So, let's set up the error handlers we'll need in the "parent class." The first method is used by the tests to say an error will be coming. It stores the expected error messages in a property and sets an error handler for PHP.
protected function expected_errors($error_messages) { $this->expected_error_list = (array) $error_messages; set_error_handler(array(&$this, 'expected_errors_handler')); }
When PHP produces an error, my error handler makes sure the message is one we anticipated. If so, the $expected_errors_found property is set to true so we can check it later.
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; }
Then there is the method that tests can call to make sure the expected PHP error actually happened and to pull my error handler off of the stack.
protected function were_expected_errors_found() { restore_error_handler(); return $this->expected_errors_found; }
Finally! Here's the test for PHPUnit to execute.
public function test_redirect() { // Establish the error handler with the expected message. $expected_error = 'Cannot modify header information'; $this->expected_errors($expected_error); // Let the customized wp_redirect() know it's okay to be called now. self::$location_expected = wp_login_url() . '?action=retrievepassword'; // Call the plugin method we want to test. self::$o->force_retrieve_pw(); // Make sure the error happened. $this->assertTrue($this->were_expected_errors_found(), "Expected error not found: '$expected_error'"); // Check that the redirect URI matches the expected one. $this->assertEquals(self::$location_expected, self::$location_actual, 'wp_redirect() produced unexpected location header.'); }
Well, we made it through. If this was helpful, buy me a beer some time. After figuring this out in the first place and pulling this post together, I can sure use one.
(I'll append it here when I get a chance.)