DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
  1. DZone
  2. Coding
  3. Languages
  4. Practical PHP Refactoring: Introduce Assertion

Practical PHP Refactoring: Introduce Assertion

Giorgio Sironi user avatar by
Giorgio Sironi
·
Oct. 31, 11 · Interview
Like (0)
Save
Tweet
Share
8.06K Views

Join the DZone community and get the full member experience.

Join For Free

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.

Assertion (software development) PHP unit test

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • DevOps for Developers — Introduction and Version Control
  • Software Maintenance Models
  • LazyPredict: A Utilitarian Python Library to Shortlist the Best ML Models for a Given Use Case
  • Metrics Part 2: The DORA Keys

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: