Programming
Article

Effortless (or Better!) Bug Detection with PHP Assertions

By Webb Stacy

If you’re anything like me, you enjoy writing code a lot more than you enjoy testing and debugging it. If we could produce higher quality code with less test and debug effort, we’d jump at the chance, right?

Assertions just might do it for us.

Don’t get me wrong. Testing and debugging are important parts of development. Give them short shrift at your peril. But the proper use of assertions leads to the earlier and more thorough detection and diagnosis of bugs. This leads to easier fixes, which frees you up for more enjoyable activities. How? Because:

  1. Bugs are harder to fix the later they are detected.
    It’s much harder and trickier to find and fix a bug in a live Website than the first time you try out the code you’ve just written. Assertions find more bugs, sooner. This saves testing and debugging time.

  2. Bugs become harder to diagnose the further the symptom is removed from the cause.
    It is often laborious to work backwards from the symptom to find the cause of a bug. In the case of intermittent bugs, it is sometimes impossible. Assertions can spot the bugs early, before any symptoms show up. In a sense, they create symptoms right where things first went wrong. This saves testing and debugging time.

  3. Assertions are easy to use.
    Most of the time it’s not much more difficult than writing comments. This adds very little testing and debugging time.

The small amount of time you invest in putting assertions in your code will pay itself back quickly, spotting bugs at a point when they are easy to diagnose and fix. And if others will use and modify your code (or you’ll come back to it yourself after a few months away), these benefits grow both for you and for those others: understanding of the code improves, support requests decrease.

What are Assertions?

Developers in the heat of battle know–or assume–certain things to be true. For example, they may know that a timestamp in a certain cache is never older than a day, that buyers won’t choose Extra Large if they are buying one-size-fits-all socks, or that there is an entry in the customers table that matches the customer id in every shopping cart.

But assumptions aren’t always correct. Sometimes, it’s as simple as not having accounted for an unusual situation. Perhaps a third-party module does let files linger for more than a day, or Extra Large is passed as a default size when no size is specified.

More often, the assumptions that were true when the program was originally written are no longer true as new functionality is added. In enhancing an online store to let people make purchases before registering, maybe there is no longer a guarantee that there’s an entry in the customers table at the moment they put something in the shopping cart.

If there are circumstances where the assumptions are not true, developers need to revisit them (and probably fix a bug.) Short of mathematical proof, the best way to find out is to use assertions.
Assertions are run-time checks that a PHP expression is true. If it is, execution continues unchanged; if not, some other action is taken. The default is to issue a warning.

They are normally checked only during development, not after the site has been deployed. This frees developers from worrying about assertion performance issues in production code.

But assertions don’t need to be removed from the code to make this happen. There is an easy way to control whether or not assertions execute, as we’ll see below. They can stay with the rest of the code and be enabled or disabled as necessary, providing built-in sanity checking when it’s time to do further work.

Using Assertions

On the surface, assertions in PHP look just like a function call:

    assert(false);

If assertions are enabled (the PHP default) this statement will cause a warning to be generated:

Warning: Assertion failed in test.php on line 43

Whereas:

assert(true);

will cause no warnings.

In PHP, the argument to assert is either a Boolean value (like the preceding examples) or a string to be evaluated as PHP code. Using the string parameter is generally more informative:

assert('false');

results in:

Warning: Assertion "false" failed in test.php on line 43

Here are two more simple examples:

$one = 1;  
$three = 3;  
assert("$one + $one == $three");  
assert('$one + $one == $three');

resulting in

Warning: Assertion "1 + 1 == 3" failed in test.php on line 45  
Warning: Assertion "$one + $one == $three"  
failed in test.php on line 46

Assertion strings work with single or with double quotes, though the warning messages differ. If you’re interested in the names of the variables showing up in the error message, use single quotes; if you would rather see the values themselves, use double quotes (making sure to follow the PHP rules about referencing variables inside double quotes).

So far, the example assertions are not very interesting since they restate the obvious. They have more meaning when they check the actual state of a program.

Here’s what an assertion to check the maximum age of a timestamp might look like:

define('MAX_CACHE_AGE', 60 * 60 * 24);  
//...  
assert('time() - $timestamp <= MAX_CACHE_AGE');

And the assertion to check that the socks’ size isn’t XL might be:
define('XL', 'Extra Large');  
//...  
assert('$socks->size() != XL');

You can check that there’s a row in the customer table for an entry in the shopping cart by writing a function, say customer_exists, and calling it in an assertion, like so:

assert('customer_exists($cart->get_customer_id())');
Where to Use Assertions

There are many ways and places to use assertions, limited only by your imagination. In general, if you would write a comment about the state of program, you can turn it into an assertion.
Of course, production code can’t depend on assertions. It must execute correctly even if all assertions were removed. This means that assertions should not do production-level error checking that requires cleanup or other action, like checking for errors after a database call or validating user input.

To illustrate some good places to use assertions, here are some specific ideas. Please don’t assume these are the only possibilities, though!

  1. Switch with no default.
    When a switch statement has no default case, often there is an implicit assumption that all the possibilities have been covered. This can be made explicit with an assertion:

    switch ($some_variable) {  
     case 'a':  
       //...  
       break;  
     case 'b':  
       //...  
       break;  
     //...  
     default:  
       assert('$some_variable != ' . "$some_variable");  
       break;  
    }

    The assertion will fail if the default case is reached, and the error message will describe the "impossible" value.

    There is a similar opportunity when a series of if-else statements is intended to cover all the cases.

  2. Type checking.
    PHP is a loosely and flexibly typed language, which serves it well most of the time. But sometimes it is important that a variable be a string, a number, an object, or of some other type.

    assert('is_resource($fp)');

  3. Parameter checking.
    Assertions can be used to check the validity of parameters passed to a routine.
    function some_function($positive_int_parameter) {  
     assert('isset($positive_int_parameter)');  
     assert('is_int($positive_int_parameter)');  
     assert('$positive_int_parameter > 0');  
     //...  
    }

    It’s important not to use assertions to check parameters if you need to do it in the production system. For instance, if the value of $positive_int_parameter could be whatever the user typed in, use normal methods to check that it is in range and do something like complain or correct the value if the check fails. If we did it with assertions, the complaint/correction would disappear when assertion checking disappeared, which would take functionality away from the production system.

    Checking function results. You can do sanity checks
    $result = process_string($input_string);  
    assert('strlen($result) <= MAX_RESULT_LEN');

    or you can actually use an alternate method of computing the function result

    $output = expensive_crunching_routine($input);  
    assert('$output == alt_expensive_crunching_routine($input)');

    Even if the alternate routine is computationally a little bit expensive, you might want to consider this kind of assertion, since it won’t be a performance issue in the production code.

  4. Multistatement assertions.
    Generally speaking, the expressions in assertions should not have side effects. One important exception is when multiple statements are required to make an assertion. For example:
    assert(list($january, $ignore) = explode(',', $year_data));  
    assert('$january > MIN_FIRST_MONTH');

    The call to explode is wrapped in an assert so that it doesn’t execute in the production system. It is guaranteed to succeed since explode only returns false when the separator is the empty string. Since it will always succeed, there is no need to pass the parameter as a string (there will be no error message). The important assertion is, of course, the second one.

    PHP does impose limits on this technique. For example, it is not possible to define a function or a class inside an assertion in order to avoid defining them in production code. Still, the performance and convenience penalty for having "helper" functions and classes still in the production code is ordinarily small, so this is not an earthshaking issue.

With practice, you’ll spot lots of opportunities for assertions.

Controlling Assertions

The routine used to control assertions in PHP is assert_options. It takes two arguments, one to specify which option to set, and the other to specify the value of that option to set. The constant to enable assertions is ASSERT_ACTIVE with possible values of 0 or 1. So during development, you’ll want to do this somewhere early on
assert_options(ASSERT_ACTIVE, 1);
but in production it should read
assert_options(ASSERT_ACTIVE, 0);
You can also control whether you terminate execution when an assertion fails (ASSERT_BAIL) and whether warnings are issued during assertion evaluation (ASSERT_QUIET_EVAL). Be careful with ASSERT_QUIET_EVAL! If it’s turned on and there’s a problem executing the assertion you wrote, perhaps because of a syntax error or because you called a function that doesn’t exist, PHP won’t tell you, and your application may behave strangely or halt when it reaches that assertion. Best let the evaluations make some "noise" by setting it to 0 (the default), unless you have a specific reason for turning it on.

Two other interesting options are ASSERT_WARNING and ASSERT_CALLBACK. These allow you to customize how your application reacts to assertions. By default, an assertion failure generates a warning like we saw earlier. But you may want to take more control over assertion handling by writing your own callback routine and installing it with ASSERT_CALLBACK. If you do so, you may choose to suppress the default warnings with ASSERT_WARNING. Let’s look at an example.

Getting Fancy

One potential problem with assertions is that they output a warning directly to the page being processed. This means trouble if you’re in a section of code generating headers-you’ll get the dreaded "Cannot add header information — headers already sent" warning. It’s also a problem if you’re in the middle of rendering a page layout, calculating values to display in a form, or performing similar UI-intensive tasks.

There’s a solution in the form of an open-source package called phpAssertUnit. It stays "out of the way" by providing a separate Assertion Reporter window to report on failed assertions. It also provides assertions capabilities for JavaScript as well as PHP.

I encourage you to explore the package, but in this article I’ll just show you how to use the Assertion Reporter to view failed PHP assertions. The basic idea is to write a PHP assertion handler that sends results to the Assertion Reporter, and to register the handler as the PHP assertion callback. Comments here explain the details.

<?php   
 include_once('assert.mod');    // phpAssertUnit PHP code  
 define('FAILED_PHP_ASSERTION',0);  // used to create a    
                                       // failed phpAssertUnit  
                                       // assertion  
 assert_options(ASSERT_CALLBACK,  
   phpAssertUnit_callback);          // register the handler  
 assert_options(ASSERT_WARNING, 1);  // set to 0 for no    
                                       // on-page warning  
 assert_options(ASSERT_QUIET_EVAL, 0); // leave off if you can      
               //  
 function phpAssertUnit_callback($file, $line, $code) {    
                                       //    
   global $Assert;                  // from phpAssertUnit  
//  
// Windows: take care of ':' because it's a trigger    
// for assertUnit, and prepare '' for HTML display  
// this should have no effect on filepaths from    
// Unix/Linux unless they contain these characters).  
//  
   $f = str_replace(':', '&#'.ord(':').';',  $file);  
   $f = str_replace('\', '\\', $f);  
//  
// If PHP assertion was called with a Boolean expression,    
// $code will be unset or empty  
// so just label it 'Boolean expression'.  
//  
   if (!isset($code) || $code == '') {  
     $code = 'Boolean expression';  
   }    
//  
// Prepare the code for HTML display...  
//    
   $c = htmlentities($code);  
//  
// ...and invoke the phpAssertUnit assertion  
//  
   $Assert->isTrue(FAILED_PHP_ASSERTION,  
true,          
//always report PHP assertions  
"PHP Assertion failed on Line $line, File $f: $c");  
 }  
?>

Include this early in your PHP file (or better yet, make it part of a config.php file that is included on every page), make sure the phpAssertUnit files (assert.mod; reporter.html; reporter.js; reporter.css) are in the right places (per the phpAssertUnit documentation and any configuration you’ve done), and you’ll have informative, unobtrusive assertion reporting in a separate window-integrated with JavaScript assertions, if you so choose.

You can see the assertion browser reporting on the assertions discussed in this article here.

Other Easy Techniques

Assertions aren’t the only tool that can help with early bug warning. Here are some others.

  1. Error Levels
    Consider setting error levels to E_ALL during development. Sure, you’ll receive some notices that may not matter, say about a variable being referenced before set when all you want to know is whether it’s NULL. But when you reference an unset variable that should have a value, you’ll be happy PHP told you about it so quickly. It’s worth developing the habit of keeping your code in a state that doesn’t trigger any warnings or notices, so when they do occur you’ll know you’ve got something to look into.

    If you choose, you can integrate notices, warnings, and errors with phpAssertUnit by adapting the code shown above and providing a suitable error handler for set_error_handler.

  2. HTML Validation
    Most PHP code generates HTML for Websites. Another easy way to find issues early is to use an HTML validator to check what your code is generating. There are many validation services available, including a good free one at http://validator.w3.org/.

  3. Unit Testing
    Modern development methodologies such as Extreme Programming advocate that you write unit tests before you write code. Whether or not you want to go this far, you’ll find you spend less time writing unit tests if you have help automating their execution. Consider using PhpUnit, a terrific open source unit test harness based on JUnit for Java.

    Incidentally, the effects of assertions and unit tests are complementary. Leave assertions enabled for the unit tests and you’ll automatically increase the number and coverage of tests on your code.

Whether your project involves one person, a small team working in the same office, a large team distributed around the globe, or something in between, assertions offer important benefits. Not only do they help you communicate your assumptions, they also tell you when your assumptions are wrong. They repay the small investment in time to create them with the big rewards of earlier and easier diagnosis and repair of problems in the code. Using them will save you time.

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Front-end, once a week, for free.