In the first part of this series, we took a high-level look at testing methodologies and gave some cases as to why it's beneficial for us to begin doing in our WordPress projects. We also took time to setup PHPUnit and the WordPress Tests in order to begin building our first testable plugin.
In this final article, we're going to define a methodology for unit testing, begin incorporating it into our work, and walk away with a fully functional (albeit simple) plugin that also has a small set of tests to ensure that it works exactly as expected.
A Unit Testing Methodology
When it comes to testing, there are generally two ways to do it:
- Write your tests, then write code to make your tests pass
- Write your code, then write tests that pass
In my experience, the first approach is always better. Granted, this is practically impossible to do within the context of an application that already exists, but if you're starting from the ground up – which we are – it's a better approach and here's why: Once you've written an application, you know how it works. As such, it can be extremely difficult to write tests that stretch the application when you inherently know how it's supposed to function.
To that end, I find it better to write the tests first. This way, your tests not only include the way the program is supposed to work, but it also becomes a form of documentation showing what functionality is intended and will ultimately yield a failure when the functionality isn't performing as it should.
With that in mind, we're going to be building with this simple methodology:
- Write a test and run it. It will obviously fail.
- Write code that attempts to cause the test to pass.
- If the test passes, we move on to the next function; otherwise, we repeat the process until it does pass.
Finally, as a refresher, our plugin is going to give a specialized welcome message to the visitor based on if they've clicked through to the site from either Google or Twitter. We'll also be writing this in such a way that it will be easier to expand with additional services, should you want to do so in the future.
Building a Testable Plugin
At this point, it's time to start writing some code; however, unlike most projects, we're not going to be jumping into WordPress-specific code just yet. Instead, we're going to stub out our unit test class. If you've structured your plugin's directory based on what we shared in the first post or how we've configured it on GitHub, then you should have a hello_reader_tests.php file located in your tests/wordpress-tests directory. You don't have to follow that organization, of course, but it will help as we progress through the project.
Let's stub out the unit test class:
require_once( '../../plugin.php' ); class Hello_Reader_Tests extends WP_UnitTestCase { } // end class
Now, attempt to run the test using from the terminal using PHP unit. Assuming that you're running PHP unit from your local MAMP installation, you should be able to enter:
$ /Applications/MAMP/bin/php/php5.3.6/bin/phpunit ./hello_reader_tests.php
At this point, you should see a failure:
That's good! It means PHPUnit is installed and running and that your WordPress Testing framework is ready to go. The test failed simply because we haven't actually written any tests. Let's get started doing that.
Our First Test
First, let's write a test to make sure that our plugin is initialized, instantiated, and ready for testing. Recall earlier in the first article that we stored a reference to the instance of Hello Reader into the PHP $GLOBALS
array. This is how we'll be accessing that instance using the testing framework. So let's update our unit test to look like this:
Note that for the sake of space, I'll be leaving out code comments but the fully commented plugin and tests will be available on GitHub.
require_once( '../../plugin.php' ); class Hello_Reader_Tests extends WP_UnitTestCase { private $plugin; function setUp() { parent::setUp(); $this->plugin = $GLOBALS['hello-reader']; } // end setup function testPluginInitialization() { $this->assertFalse( null == $this->plugin ); } // end testPluginInitialization } // end class
Above, we've setup a reference to the instance of the plugin so that we can access it throughout our unit tests. We're using the setUp
method to grab the reference to the plugin from $GLOBALS
. Note, however, that we've introduced another function called testPluginInitialization
. This function verifies that the reference we've setup in the setUp
method is not null.
If you rerun the tests, you should now get a passing test and your terminal should look like this:
There is an important takeaway here: Note that the single function we've provided above has a clear purpose: to verify that the plugin has been properly initialized. Its function name is clear and contains a single assert statement. This is a great way to model our remaining tests primarily because it makes it easy to find bugs when they appear. Think about it this way: If you include a number of different assert statements in a single function, it's going to be difficult to determine which assert statement is failing.
The First Function
Now that we've gotten introduced to how to write unit tests, run unit tests, and evaluate how they pass or how they fail, let's begin implementing functionality for the plugin. First off, we're going to need to setup a filter for the content since we're going to be appending text to the beginning of the content. In following with the methodology that we defined earlier in this article, let's write our test first.
This particular test is going to look to see if we've appended a specific set of text to the first part of the post:
function testAddWelcomeMessage() { $this->assertEquals( 'TEST CONTENT', $this->plugin->add_welcome_message( 'This is example post content. This simulates that WordPress would return when viewing a blog post.' ), 'add_welcome_message() appends welcome message to the post content.' ); } // end testAddWelcomeMessage
If you run the test exactly as it is, it won't even fail – instead, PHPUnit will return a fatal error because the method isn't defined in the plugin. So let's add that now. Update the plugin to look like this:
class Hello_Reader { function __construct() { add_filter( 'the_content', array( &$this, 'add_welcome_message' ) ); } // end constructor public function add_welcome_message( $content ) { } // end add_welcome_message } // end class
Now attempt to run the test. The test won't bomb, but you should actually see a failure along with a clear message as to why the test failed:
1) Hello_Reader_Tests::testAddWelcomeMessage add_welcome_message() appends welcome message to the post content. Failed asserting that null matches expected 'TEST CONTENT'
So, in keeping with our methodology, we want to make this test pass. In order to do so, we need to make sure that the post content contains the string of text – in this case, 'TEST CONTENT
', in order to make it pass. So let's try this. Update the corresponding function in the plugin to append the string before the content:
public function add_welcome_message( $content ) { return 'TEST CONTENT' . $content; } // end add_welcome_message
And again, we rerun the test only to see that it fails. If you notice our test, this is because it's looking to see of our content equals the 'TEST CONTENT
' string. Instead, we need to make sure that the string starts on the content. This means that we need to update our test. Luckily, PHPUnit has an assertContains function. So let's update our code to use it:
function testAddWelcomeMessage() { $this->assertContains( 'TEST CONTENT', $this->plugin->add_welcome_message( 'This is example post content. This simulates that WordPress would return when viewing a blog post.' ), 'add_welcome_message() appends welcome message to the post content.' ); } // end testAddWelcomeMessage
Once again, rerun the test and you should see that the test now passes. Awesome! Now we need to write customized messages for people coming from Twitter and people coming from Google.
Welcoming Our Twitter Visitors
There are a number of different ways that we can check to see how a user has arrived at a given page. Sometimes we can check the values in the $_GET
array, sometimes we can interrogate the $_SERVER
array, or sometimes we can check a user's session. For purposes of this example, we're going to be looking for 'twitter.com' to be found in the $_SERVER['HTTP_REQUEST']
. I say this just so you guys can follow along with what we're doing in the code.
So, generally speaking, the add_welcome_message
should check to see if the request has come from Twitter and then tailor the message appropriately. Since we're in the business of testing each piece of functionality, we can write a function that can evaluate if the request is coming from Twitter. So let's write a new test:
In the plugin:
public function is_from_twitter() { } // end is_from_twitter
In the test:
function testIsComingFromTwitter() { $_SERVER['HTTP_REFERER'] = 'http://twitter.com'; $this->assertTrue( $this->plugin->is_from_twitter(), 'is_from_twitter() will return true when the referring site is Twitter.' ); } // end testIsComingFromTwitter
We're obviously spoofing the HTTP_REFERER
value, but that's okay for purposes of this example. The point still remains: run the test, it will fail, and so we'll need to implement the function in the plugin to have it pass:
public function is_from_twitter() { return strpos( $_SERVER['HTTP_REFERER'], 'twitter.com' ) > 0; } // end is_from_twitter
Rerunning the test should now result in a passing test. But wait – we need to be complete. Let's make sure that we run a test to verify that this function fails when the referrer is not from Twitter.
function testIsNotComingFromTwitter() { // Spoofing the HTTP_REFERER for purposes of this test and the companion blog post $_SERVER['HTTP_REFERER'] = 'http://facebook.com'; $this->assertFalse( $this->plugin->is_from_twitter(), 'is_from_twitter() will return true when the referring site is Twitter.' ); } // end testIsNotComingFromTwitter
Notice that we've updated the HTTP_REFERER
and we've changed assertTrue
to assertFalse
. Permitting everything else is correct, run the tests and they should pass.
Repeating the Same for Google
Providing a customized message for Google will require the same thing that we did for Twitter, that is, spoof the HTTP_REFERER
and then return true or false for the helper function. So, in order to avoid sounding redundant, I'll keep this section as concise as possible. The same steps must be followed as for Twitter.
First, we stub out the helper function in the plugin:
public function is_from_google() { } // end is_from_google
Then we stub out the test:
function testIsComingFromGoogle() { $_SERVER['HTTP_REFERER'] = 'http://google.com'; $this->assertTrue( $this->plugin->is_from_google(), 'is_from_google() will return true when the referring site is Google.' ); } // end testIsComingFromGoogle
Running the test as it is right now will result in a failure. So, let's implement the is_from_google()
function:
public function is_from_google() { return strpos( $_SERVER['HTTP_REFERER'], 'google.com' ) > 0; } // end is_from_twitter
And now, the test should pass. But, again, we need to be complete so let's write the failure test to assume that the function won't return true when users are coming from somewhere else:
function testIsNotComingFromGoogle() { // Spoofing the HTTP_REFERER for purposes of this test and the companion blog post $_SERVER['HTTP_REFERER'] = 'http://facebook.com'; $this->assertFalse( $this->plugin->is_from_google(), 'is_from_google() will return true when the referring site is Google.' ); } // end testIsNotComingFromGoogle
Finally, run your tests. Permitting everything else is correct, you should have six passing tests.
Pulling It All Together
At this point, we've got all we need to begin displaying custom welcome messages to our users. The only thing is that we'll need to refactor our initial test that's checking for "TEST CONTENT." Now, we'll need to introduce tests for the following cases:
- When the user comes from Twitter, we'll say "Welcome from Twitter!"
- When the user comes from Google, we'll say "Welcome from Google!"
- When the user comes from anywhere else, we won't prepend anything.
So let's remove the test we created earlier testAddWelcomeMessage
in place of adding three new tests.
First, we'll add a test that checks the Twitter welcome message.
In the plugin, we'll reduce the add_welcome_message
to this:
public function add_welcome_message( $content ) { return $content; } // end add_welcome_message
And we'll add the Twitter test, first:
function testDisplayTwitterWelcome() { // Spoof the HTTP_REFERER for Twitter $_SERVER['HTTP_REFERER'] = 'http://twitter.com'; $this->assertContains( 'Welcome from Twitter!', $this->plugin->add_welcome_message( 'This is example post content. This simulates that WordPress would return when viewing a blog post.' ), 'add_welcome_message() appends welcome message to the post content.' ); } // end testDisplayTwitterWelcome
At this point, this is old hat, right? Run it, the test will fail. Implement the add_welcome_message
to look like this:
public function add_welcome_message( $content ) { if( $this->is_from_twitter() ) { $content = 'Welcome from Twitter!' . $content; } // end if return $content; } // end add_welcome_message
Run it again, and it'll pass. Next up is the Google test:
function testDisplayGoogleWelcome() { // Spoof the HTTP_REFERER for Google $_SERVER['HTTP_REFERER'] = 'http://google.com'; $this->assertContains( 'Welcome from Google!', $this->plugin->add_welcome_message( 'This is example post content. This simulates that WordPress would return when viewing a blog post.' ), 'add_welcome_message() appends welcome message to the post content.' ); } // end testDisplayGoogleWelcome
Run the test, have it fail, then update the add_welcome_message
in the plugin to contain a check using the helper function we wrote earlier:
public function add_welcome_message( $content ) { if( $this->is_from_twitter() ) { $content = 'Welcome from Twitter!' . $content; } else if( $this->is_from_google() ) { $content = 'Welcome from Google!' . $content; } // end if return $content; } // end add_welcome_message
At this point, you should have a fully functional plugin that has seven passing unit tests!
Conclusion
As you can see, unit testing introduces an additional level of development but can pay off significantly in maintainable, well-organized, and testable code. As your application grows, continually running tests to verify that your projects work as expected can give piece of mind. Of course, this is but a small example of how unit testing works. Applying these practices can pay off in much larger and/or complicated projects.
Finally, you can find this plugin, the WordPress Tests, and the Hello Reader unit tests fully commented on GitHub.
Comments