Programming
Article
By Chris Cornutt

Tutorial: Introduction to Unit Testing in PHP with PHPUnit

By Chris Cornutt
Help us help you! You'll get a... FREE 6-Month Subscription to SitePoint Premium Plus you'll go in the draw to WIN a new Macbook SitePoint 2017 Survey Yes, let's Do this It only takes 5 min

It’s a familiar situation: you’ve been developing on an application for hours and you feel like you’ve been going round in circles. You fix one bug and another pops up. Sometimes, it’s the same one you found 30 minutes ago, and sometimes it’s new, but you just know it’s related. For most developers, debugging means either clicking around on the site, or putting in a whole load of debugging statements to hunt the problem down.

You’ve been there, right? You’ve had these same frustrations with all your applications, and have sat back and thought that there has to be a better way. Well, you’re in luck! There is, and it’s not as difficult as you might think it. Unit testing your application will not only save you a lot of headaches during development, but it can result in code that’s easier to maintain, allowing you to make more fearless changes (like major refactoring) without hesitation.

The key to understanding unit testing is to define what we mean by “unit.” A unit is simply a chunk of functionality that performs a specific action where you can test the outcome. A unit test, then, is a sanity check to make sure that the chunk of functionality does what it’s supposed to.

Once you’ve written up your set of tests, whenever you make a change to your code, all you have to do is run the set of tests and watch everything pass. That way, you can be reassured that you haven’t inadvertently broken another part of your application.

Debunking Unit-testing Myths

I’m sure you’re sitting there thinking, “if this unit testing stuff is so awesome, why doesn’t everyone do it for all their applications?” There are a few answers to that question, but none of them really are very good excuses. Let’s run through the common objections, and explain why they’re far from compelling reasons to avoid writing tests.

It Takes Too Long

One of the largest concerns about writing up tests is that they just take too much time to generate. Sure, some of the IDEs out there will autogenerate a set of basic tests for you; but sitting down and writing good complete tests for your code takes some time. Like many best practices in development, a little investment of time to do tasks the right way can save you a lot of time over the life of your project. Writing a solid test suite is definitely one of those cases. Moreover, you’re probably already testing your code by visiting your site and clicking around every time you add new features. Running an established test suite can be much faster than manually testing all your functionality.

There’s No Need to Test: My Code Already Works!

Another common statement you hear from developers about writing up a set of tests is that the application works, so there’s no real need to test it. They know the application, and they know right where to drop in and fix a bug, sometimes in seconds. But drop a shiny new developer into the cubicle next to them, and you might start to see why those tests would be a good idea. Without them, the newbie could go changing code without a care in the world, and break who knows what. With a repeatable test run, some of those bugs could be avoided.

It’s No Fun

The final reason why developers don’t like writing tests is that it’s just not much fun to write them. Developers, by their nature, want to solve problems. Writing code is like forming something out of nothing, creating order out of chaos to make something useful. As a result, they see writing tests as boring—a task they might get to one of these days if they have time after the real work is done. So they miss out on the value testing has even during development (especially during development!) to keep the process consistent and clean. Look at it this way: no one thinks that wasting hours chasing a pernicious bug is fun, and testing enables you to put in a little effort up front to avoid a lot of frustration down the track.

An Example

Now we get to the good part—a practical example that you can sink your teeth into. For the sake of my examples, I’m only going to use one of the more popular unit-testing tools out there, PHPUnit. This amazing application was developed by Sebastian Bergmann, and it provides an excellent set of features to help make testing your code a snap.

If you’re yet to install PHPUnit, the simplest way to grab it is from its PEAR channel.

pear channel-discover pear.phpunit.de
pear channel-discover components.ez.no
pear channel-discover pear.symfony-project.com
pear install phpunit/PHPUnit

If all goes well, you’ll have all the tools you need installed. The PEAR installer will grab any dependencies you might need for it to run.

If you want to install it manually, there are instructions in the PHPUnit manual. Note, though, that you’ll need a working PEAR installation to use PHPUnit. It relies on several other PEAR libraries to work and, if you don’t have the things it needs in place, it’ll throw some errors when you try to run the phpunit binary. The manual installation process is a bit more work, but, trust me, your fully tested consistent code will thank you for it.

Writing a First-test Case

With PHPUnit, the most basic thing you’ll write is a test case. A test case is just a term for a class with several different tests all related to the same functionality. There are a few rules you’ll need to worry about when writing your cases so that they’ll work with PHPUnit:

  • Most often, you’ll want to have your test class extend the PHPUnit_Framework_TestCase class. This gives you access to built-in functionality like the setUp() and tearDown() methods for your tests.
  • The name of the test class needs to mimic the name of the class you’re testing. For example, to test RemoteConnect, you’d use RemoteConnectTest.
  • When you create the test methods, you need to always start them with “test” (as in testDoesLikeWaffles(). The methods need to be public. You can have private methods in your tests, but they won’t be run as tests by PHPUnit.
  • The test methods will never receive any parameters. When you write your tests, you need to make them as self-contained as possible, pulling in what they need themselves. This can be very frustrating sometimes, but it leads to cleaner, more effective tests.

We need to start with some functionality to test, so here’s the class we’ll be working with in the following examples. It’s fairly basic just to keep it simple. Here’s what goes in the RemoteConnect.php library:

<?php
class RemoteConnect
{
  public function connectToServer($serverName=null)
  {
    if($serverName==null){
      throw new Exception(“That's not a server name!”);
    }
    $fp = fsockopen($serverName,80);
    return ($fp) ? true : false;
  }

  public function returnSampleObject()
  {
    return $this;
  }
}
?>

So, for example, if we were going to test this functionality to make a request to a remote server, our test might look like this:

<?php

require_once('RemoteConnect.php');

class RemoteConnectTest extends PHPUnit_Framework_TestCase
{
  public function setUp(){ }
  public function tearDown(){ }

  public function testConnectionIsValid()
  {
    // test to ensure that the object from an fsockopen is valid
    $connObj = new RemoteConnect();
    $serverName = 'www.google.com';
    $this->assertTrue($connObj->connectToServer($serverName) !== false);
  }
}
?>

You’ll notice that the class extends the base PHPUnit test case, so a lot of excellent features come along with it. Those first two methods—setUp and tearDown—are examples of this kind of built-in functionality. They are helper functions that are executed as part of the normal test run. They’re run before all the tests, and after they’re all done, respectively. Despite being handy, we won’t worry about them yet. The real focus is our testConnectionIsValid method. Our method sets up our environment by creating a new instance of our RemoteConnect class, and calls the connectToServer on it.

Now, on to the real business of our test. See that assertTrue in there? That’s one of PHPUnit’s helper functions, of which there are quite a few. assertTrue is the simplest assertion: all it does is check to see if a Boolean expression is true. Other helper functions can test for object properties, file existence, the presence of a given key in an array, or a match of a value to a regular expression, to name just a few. In this case, we want to be sure that the connectToServer result isn’t false—that would mean that our connection had failed for some reason.

Running Tests

Running your tests is as simple as calling the phpunit executable and pointing it at your tests. Here’s an example of calling our test from above:

phpunit /path/to/tests/RemoteConnectTest.php

Simple, right? The output is just as basic: for each of the tests in your test case, PHPUnit runs through them and gathers some statistics like pass, fail, and number of tests and assertions made. Here’s an example of the output from our example run:

PHPUnit 3.4 by Sebastian Bergmann
.
Time: 1 second
Tests: 1, Assertions: 1, Failures 0

For each test that’s run, you’ll either see a period (.) if it’s successful (as above), an “F” if there’s a failure, an “I” if the test is marked as incomplete, or an “S” if it’s been marked as skipped.

By default, PHPUnit is configured to run through a whole set of tests at once and report back the total statistics in one easy report.

Our example test shows a passing test—so we know that, provided with the correct parameters, our method functions as expected. But, you also need to be sure to test for when things go wrong. What happens if the host name you provide to the method doesn’t exist? Does the method throw an exception as we’d like it to? Be sure that when you’re writing tests, you have checks for the positive as well as the negatives.

In the case of the connectToServer method from our example class, providing an invalid host name for the connection will throw an exception. Handling exceptions with PHPUnit is outside the scope of this article, but I’d recommend reading the relevant section of the PHPUnit documentation if you want to dig into it.

There are several different assertions that can help you test the results of all sorts of calls in your applications. Sometimes you have to be a bit more creative to test a more complex piece of functionality, but the assertions provided by PHPUnit cover the majority of cases you’d want to test. Here’s a list of some of the more common ones you’ll find yourself using in your tests:

AssertTrue/AssertFalse Check the input to verify it equals true/false
AssertEquals Check the result against another input for a match
AssertGreaterThan Check the result to see if it’s larger than a value (there’s also LessThan, GreaterThanOrEqual, and LessThanOrEqual)
AssertContains Check that the input contains a certain value
AssertType Check that a variable is of a certain type
AssertNull Check that a variable is null
AssertFileExists Verify that a file exists
AssertRegExp Check the input against a regular expression

For example, let’s say we get back an object from a method (like the one our returnSampleObject method provides) and we want to see if it’s an instance of a particular class:

<?php

function testIsRightObject() {
  $connObj = new RemoteConnect();
  $returnedObject = $connObj->returnSampleObject();
  $this->assertType('remoteConnect', $returnedObject);
}

?>

Our method was written to return the class itself, so this test should pass and we can go on our merry way.

One Assertion per Test

As with any area of development, there are a few best practices worth adhering to when writing tests. An important one is the idea of “one test, one assertion.” This school of thought says that for each of your tests, there can be only one check or assertion. Our test examples so far have followed this principle: each test only called one assertion method. Some developers, however, think that this can be a waste of test space: “Hey, while we’re in there—let’s test for this as well.” Here’s an example:

<?php

public function testIsMyString(){
  $string = “Mostly Harmless”;
  $this->assertGreaterThan(0,strlen($string));
  $this->assertContains(“42”,$string);
}

?>

Our fun little testIsMyString example is testing for two different things. First, it checks that the string is nonempty (length greater than 0), and then it checks that the string contains the number “42.” Right away, you can see how this might become tricky: the test would fail if the string was, for example, “fortytwo.” But you’d see exactly the same failure if the string was empty, which might be caused by an entirely different bug. The “fail” result can be deceptive and might cause some confusion as to what it’s really reporting.

Framework Support for Unit Tests

Several of the popular PHP-based frameworks out there (such as the Zend Framework and Symfony) have included the ability to write tests against their functionality. Because MVC frameworks involve quite a bit more than what you’d find in a simple PHP script or library, they’ve provided hooks into the framework to assist in writing tests.

It might make more sense if you see an example. Let’s look at a test using the Zend Framework, verifying the routing of a URL to a controller:

<?php

class CommentControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
  public function setUp()
  {
    parent::setUp();
  }

  public function tearDown()
  {
    parent::tearDown();
  }

  public function appBootstrap()
  {
    $this->frontController->registerPlugin(new Initializer('test'));
  }

  public function testGoHome()
  {
    $this->dispatch('/home');
    $this->assertController('home');
  }

}

?>

Of course, this example is a little silly, since we’re testing built-in framework functionality rather than our own code, but you get the idea. Your test extends a different test case class, Zend_Test_PHPUnit_ControllerTestCase, so that it knows how to test a Zend Framework controller. You’ll notice, though, that we’re still essentially using PHPUnit here. Most of the test will feel familiar, but you have access to a few special assertions, like assertController used above. You can find the complete documentation about the Zend_Test component in the Zend Framework manual.

Test-driven Development

I’d be remiss if I talked about testing without mentioning a growing technique used by many developers: test driven development. Test-driven development (TDD) is a technique used during development. The basic idea behind TDD is that you write the tests first, before a single line of application code is even written. But wait, how do you know what to put in the tests without the working code to look at? Well, that’s the point. In TDD you write the test to check for the anticipated functionality, and then write the code to match it. When you start out and have your first set of tests, they’ll all (obviously) fail. As you write your code, you work until the full test case is “green” and everything passes. This method lets you focus more on the requirements first, rather than getting lost in the minutia of your code.

This can be a difficult method for a newbie to unit testing to pick up. If that describes you, I’d recommend writing against current code first, so that you can gain a feel for tests and using PHPUnit. Then, if you want to jump to the other side, you can start out your next project with TDD. Be aware that it will be slow the first time. Thankfully, you can carry over the knowledge you gathered in testing, and write better-informed tests that cover the right functionality.

Summary

I hope I’ve given you a good introduction to the world of unit testing. Even though there are a multitude of topics I’ve not touched on, I’ve tried to give you a good jumping off point where you can go and start writing your own tests right now (you are going to write some tests, right?). Even if it’s just a few tests here and there, you can ease your development fears just a bit. Soon enough, you’ll reach a point where you won’t be able to imagine refactoring your code without them.

More:
PHP
Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the most important and interesting stories in tech. Straight to your inbox, daily.Is it good?