I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 637 posts at DZone. You can read more from them at their website. View Full User Profile

Practical PHP Refactoring: Introduce Assertion

10.31.2011
| 5132 views |
  • submit to reddit

A portion of code makes an assumption about something: the current state of the object, or of the parameter, or of a local variable of the cycle. Normally this assumption would never be violated, but can be in case a bug is introduced.

Let's make assumptions explicit as we do for type hints on method parameters; the rationale is the same as the check serves both to ensure correctness and for documentation purposes. This documentation is kept as an assertion embedded in the production code (which is different from the assertions made in tests, although the behavior is the same.)

Why an assertion?

If the assumption isn't true and the code would produce a nonsensical result, it's better to stop immediately (for example by raising an exception). Fowler's advice is to use assertions only for things that need to be true, not for all things which are true in a particular point of the program (which are infinite.)

If the code works with the assertion failing, you should remove it as it is not an assumption that the code is making. Otherwise, it means you have a missing test...

Use cases

I try to make assertions on unreachability of certain parts of code which must stay there due to how the language works. For example, when an implicit return null is reached:
public function doSomething() {
    if ($blue) { ... }
    else if ($notBlue) { ... }
    assert('false');
}

It's better to execute an assertion which will raise an exception in the worst case than returning null and getting a fatal error for calling (null)->method().

You can also make assertions on the number of elements in a collection before getting the first ones, which would give a nicer error in case of problems:

assert('count($array) >= 2');
$variable = $array[0] + $array[1];
...

Assertion vs. exceptions

When assertions become bad? When an exception is better. Exception classes can be chosen in order to provide precise report of the error, while assertions usually throw always the same generic exception (see next section).

In fact, some exceptions may be an evolution of assertions (with the catch that they can be caught... what a horrible pun.) There are many views on assertions vs exceptions, but in my opinion unrecoverable exceptions are equivalent to assertions (apart from typing) as they indicate a bug and should never happen.

In PHP

A common rule of thumb is to use exceptions for other people's errors like precondition on input, and assertions for your own errors like bugs in the code.

In fact, I used them back in the days of cowboy coding in the middle of complex code, to easily isolate a bug. Now that we test everything with PHPUnit assertions are less useful. However, assert() checks can be removed in production code (by disabling the assert() handler).

The PHP case is also peculiar because often a bug just stops a HTTP request from being answered, instead of halting the whole application. But in Ajax applications the client side may not be equipped to handle error in the response returned by the server.

Assumptions?

In the debate between self testing code and code exercised by an external test suite, it's important to consider the locality of the assertions. If you make as assumption about the presence of a singleton, it's not very good to put an assertion in the production code. It would be better to improve the code by making it more local and question that assumption.

The test suite substitutes defensive programming in many cases to simplify testability, as it collects almost all the assertions you are gonna make. I suggest to write assertions only for data that comes from the integration of a group of objects not under your control, or for data deep inside a computation that you cannot easily expose to a unit test.

It's important to ensure the quality of code deployed in production, but also to separate the concerns of testability from the concerns of functionality. Add to that the beneficial effects of testability on design and now you know why a battery of unit test is almost always superior to embedded assert().

After all, you would remove scaffolding from a finished building.

Example

in this example we see how to add an assertion over the current state of an object before. then we transform it in a custom exception, to see how this change can be done easily in case it applies to your code. In the initial state, there is nothing stopping the tax rate from becoming negative:

<?php
class IntroduceAssertion extends PHPUnit_Framework_TestCase
{
    public function testTaxesAreAddedToTheNetPrice()
    {
        $price = new Price(1000);
        $price->addTaxRate(20);
        $this->assertEquals(1200, $price->value());
    }

    public function testTaxesCanBeLoweredBy10PerCentAtTheTime()
    {
        $price = new Price(1000);
        $price->addTaxRate(20);
        $price->lowerTaxRate();
        $price->lowerTaxRate();
        $this->assertEquals(1000, $price->value());
    }
}

class Price
{
    private $net;
    private $taxRate;

    public function __construct($net)
    {
        $this->net = $net;
    }

    public function addTaxRate($rate)
    {
        $this->taxRate = $rate;
    }

    public function lowerTaxRate()
    {
        $this->taxRate -= 10;
    }

    public function value()
    {
        return $this->net * (1 + $this->taxRate / 100);
    }
}

We add a test to check this corner case (which now will fail). The goal is just to show the behavior of assertions in this example: a test shouldn't be needed in real code once you're familiar with assert().

    public function testTaxesCannotBeLoweredBelowZeroForAValidPrice()
    {
        $price = new Price(1000);
        $price->addTaxRate(20);
        $price->lowerTaxRate();
        $price->lowerTaxRate();
        $price->lowerTaxRate();
        $this->setExpectedException('AssertionException');
        $price->value();
    }

Now we introduce the assertion. We have to configure a callback that assert() will call after an expression evaluates to false. The argument of assert() is expressed as a string so that it can be reported to the programmer when the assertion fails (passing a boolean will not result in the same behavior).

<?php
class AssertionException extends Exception {}
assert_options(ASSERT_CALLBACK, function($file, $line, $message) {
    throw new AssertionException($message);
} );

class Price
{
    private $net;
    private $taxRate;

    public function __construct($net)
    {
        $this->net = $net;
    }

    public function addTaxRate($rate)
    {
        $this->taxRate = $rate;
    }

    public function lowerTaxRate()
    {
        $this->taxRate -= 10;
    }

    public function value()
    {
        assert('$this->taxRate >= 0');
        return $this->net * (1 + $this->taxRate / 100);
    }
}

Here is the same behavior obtained via a Factory Method on the assertion class. This time we use directly exceptions; note that we invert the logic and we don't have code inside a string anymore.

<?php
class AssertionException extends Exception
{
    public static function throwIf($condition)
    {
        if ($condition) {
            throw new self('Assertion failed.');
        }
    }
}

class Price
{
    private $net;
    private $taxRate;

    public function __construct($net)
    {
        $this->net = $net;
    }

    public function addTaxRate($rate)
    {
        $this->taxRate = $rate;
    }

    public function lowerTaxRate()
    {
        $this->taxRate -= 10;
    }

    public function value()
    {
        AssertionException::throwIf($this->taxRate < 0);
        return $this->net * (1 + $this->taxRate / 100);
    }
}

I'm quite favorable to exceptions in any case where the object isn't in a perfectly defined state, as the alternative is to produce garbage as a result. If I had to continue working on this code, I will move the exception to the lowerTaxRate() method to catch the problem earlier.

Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)