Writing Stupid Simple Unit Tests

In this post, we'll cover the basics of unit tests and walk through the process determining what types of tests you should write for your code. We'll also write a few tests to show just how simple most unit tests are.



The aim here is to make tests a bit more familiar and less daunting and mysterious. Unit tests are good. I'm by no means an expert on software testing. I'm just someone that's discovered the joy and freedom that writing tests provides.



If you are an expert on software testing and notice I'm wrong, please tell me!

Onward!

What are unit tests?

If you’re not familiar with unit tests, they’re essentially software written to test discrete units of functionality in a system.

Unit tests test the expected behavior of your software as it exists (or existed) at a specific point in time. They make sure that everything works as expected based on the rules established for the software at the time it was written.

If those rules change -- and your software along with them -- your tests will fail. When they fail, you'll gain insight into how the change affects the rest of the system.

For example, if a change is made in code for your site’s user profile management, you'll want to make sure that the change does not affect the behavior of your site’s payment processing code.

Tests can save time and money by acting as the first line of defense in catching problems before they result in a late night emergency or catastrophic failure. When things go wrong, they can also serve as the first response team. You can run your unit tests to see if any rules/expectations have been broken by some recent change, either in your code or some external API dependency, and fix the bug introduced by the change.

Sometimes the tests become outdated. Again, the tests will fail, letting you know that you’re no longer testing for the right thing. When this happens, you should update your tests to make sure they are in line with the new rules for your software.

Getting started

Unit tests are rarely difficult to write or run if you’re using a testing framework (e.g., phpunit).

You can write your tests before you write your business code if Test-driven Development (TDD) is your thing or you can write them after.

It doesn’t matter much when you write them as long as you have them.

The brilliant thing is that almost all tests you write will be stupid simple.

Tests usually assert some condition is true (or false, depending on what outcome you expect).

On top of that, most testing frameworks have convenience functions built in for performing these types of tests and several others, including testing the expected output of a function (i.e., what the function prints to the screen).

The three test functions we've used most frequently when writing tests for Largo are assertEquals, assertTrue and expectOutputString.

Our stupid simple example plugin

So, let’s say that we want to write tests for a WordPress plugin. Our plugin simply prints the message “Hello world!” in the top corner of the WordPress dashboard (sound familiar?).

This is the extent of the code:

<?php
/**
 * @package Hello_World
 * @version 0.1
 *
 * Plugin Name: Hello World!
 * Plugin URI: https://gist.github.com/rnagle/d561bd58504a644e9657
 * Description: Just a simple WordPress plugin that prints "Hello world!" in the top corner of the WordPress dashboard.
 * Author: Ryan Nagle
 * Version: 0.1
 **/

function hello_world() {
  return "Hello world!";
}

function hello_world_markup() {
	echo "<p id='hello-world'>" . hello_world() . "</p>";
}
add_action('admin_notices', 'hello_world_markup');

function hello_world_css() {
	$x = is_rtl() ? 'left' : 'right';

	echo "
	<style type='text/css'>
	#hello-world {
		float: $x;
		padding-$x: 15px;
		padding-top: 5px;
		margin: 0;
		font-size: 11px;
	}
	</style>
	";
}
add_action('admin_head', 'hello_world_css');

Couple of things to note:

  1. We have one function -- hello_world -- that returns a string.
  2. Two other functions -- hello_world_markup and hello_world_css -- that echo strings but have no return statements. This means we'll have to check the output of these functions rather than the return value to properly test them.
  3. Note that hello_world_markup relies on hello_world to provide the the “Hello world!” message.

So, how do we test this plugin? Well, if you don’t have your test framework installed and configured, you’ll want to get that ready and raring. The details are beyond the scope of what we'll cover here, but for an overview of setting up unit tests for WordPress plugins and themes, see Will Haynes’ Unit Testing Themes and Plugins in WordPress.

The INN Nerds also have a collection of deploy tools to make developing WordPress sites and writing tests much easier. You can read about using the deploy tools for testing themes and plugins here: Updates to INN’s Deploy Tools.

Disclaimer: getting unit tests working WordPress can be daunting at first. It’s a lot to digest and may take some time to set up if you’re coming to this cold, so be patient. It’ll be worth it.

If you’re really dedicated to the cause and need a hand, reach out to us.

How do I write a unit test?

With that out of the way, let’s design some unit tests for our “Hello World!” plugin.

The first thing we’ll want to do is enumerate the features of the code which we need to test.

There are many ways you can approach this. My preference is to create a single test file per file in my source code and have my tests directory mirror the structure of my source directory.

Each test file has tests for each function (or member functions of classes) in the corresponding source file.

So, the directory structure for our plugin would look like:

hello-world/
   hello-world.php
   phpunit.xml
   tests/
       bootstrap.php
       test-hello-world.php

We’ll be doing our work in test-hello-world.php where we’ll set up the skeleton of our test case, which is as simple as extending WP_UnitTestCase:

<?php

class HelloWorldTest extends WP_UnitTestCase {}

We can then stub out test functions for each function:

 class HelloWorldTest extends WP_UnitTestCase {
  function test_hello_world() {
    // Test hello_world()
  }

  function test_hello_world_markup() {
    // Test hello_world_markup()
  }

  function test_hello_world_css() {
    // Test hello_world_css()
  }
} 

Now, let's look at each function and consider what we're testing:

1. For hello_world, we want to verify that the string returned is "Hello World!":

function test_hello_world() {
  $this->assertEquals("Hello World!" == hello_world());
}

Easy enough. Now if for some reason someone changes the return value to "Hello world!" -- capitalization be damned -- the test will fail.

I can hear you now, "This is stupid, no one writes code like this." Yes, the example is stupid and that's the point. Don't get caught up focusing on the wrong thing.

It makes no difference how complex the function is, the only concern is verifying that the return value or the outcome is what is expected.

So, if instead you're testing some_made_up_function which returns an object, you may want to verify that it is actually a PHP Object:

$this->assertEquals(gettype(some_made_up_function()), "object");

Or that the object has a specific member attribute:

$test_obj = some_made_up_function();
$this->assertTrue(property_exists($test_obj, 'name_of_attribute_goes_here'));

2. For hello_world_markup, we want to verify that the function prints "<p id='hello-world'>Hello World!</p>":

function test_hello_world_markup() {
  $this->expectOutputString("<p id='hello-world'>Hello World!</p>");
  hello_world_markup();
}

Notice that we're expecting "Hello World!" to be part of the output. This might not be a good thing. If the return value of hello_world changes, this test will fail, too.

For the sake of example, let's say we only care about testing the markup and not the message. We can take this test a step further and decouple the two so that we're only testing the markup by changing it to read:

function test_hello_world_markup() {
  ob_start();
  hello_world_markup();
  $result = ob_get_contents();
  ob_end_clean();

  $this->assertTrue(!empty(preg_match("/^<p\s+id='hello-world'>.*<\/p>$/", $result)));
}

Essentially what we're saying is, we don't care what the message is as long as it is wrapped in <p id="hello-world" /> tag.

Simple, right?

3. For hello_world_css, we want to verify that the function prints the CSS rules for our "Hello World!" markup:

function test_hello_world_css() {
  ob_start();
  hello_world_css();
  $result = ob_get_contents();
  ob_end_clean();

  // Make sure there are style tags being printed. Duh.
  $this->assertTrue(!empty(preg_match("/.*?.*<\/style>/s", $result)));

  // Make sure we're using the right selector
  $this->assertTrue((bool) strpos('#hello-world', $result)));
}

And with that, we're done! You can see the entirety of test-hello-world.php here.

When to write tests

As mentioned earlier, you may want to write your tests first. This is called Test-driven Development.

Writing your tests first has lots of awesome benefits. When you write tests first you are forced to think about the design of your system and how best to structure the software so that it is actually testable. It’s a good practice and will help increase the orthogonality of your code.

However, it's never too late to start writing tests. When you write tests for existing code, you'll find places where things need to be refactored (sometimes completely rewritten or redesigned) to clean up the spaghetti code.

If you really can't afford the time it takes to write tests for all your code (or aren't being allotted the time to do so), you might still be able to lobby for time.

It's extremely important that you can verify your code works, especially in cases where you're using code to make an assertion about the world. For example, if you're writing code that processes a data set which will later be used in a story or investigation, it's crucial that your code produces results that are precise and correct.

The other case where you should absolutely be writing tests is for critical customer interactions. Saving sensitive information or processing payment information are things that you don't want to get wrong.