Namespaces, unit testing and dependency injection (with typehinting)

Bad Medicine by Vermin Inc @ Twitter
Injection Dependency STRAIGHT INTO YOUR OCCIPITAL LOBE!

I’ve struggled with the concept of unit testing and how to deal with dependencies a lot in the past. The solution seemed to be just out of reach. I knew there WAS a solution and not a hacky one either, one that was elegant and worked well. I never really found it. PHPUnit seemed to be well written but not particularly well documented and a lad like myself, one who is a few sandwiches short of a picnic at times, almost gave up.

Almost.

I know and understand the benefits of unit testing and I understand the disadvantages. Unit testing is a weapon that can mite out retribution and vengence if not used properly. There are plenty of posts on the web about the wrong way to unit test and the hazards of them, so I’ll leave you to find them.

Aaaaaanyway, writing your application so that it uses the dependency injection pattern is the best method for making sure it’s uncoupled and modular enough to write some efficient, unit tests (ie: tests that test just a unit of code). Especially when they depend on objects which link to databases, or web services or other crazy stuff that you don’t really want or need to instantiate in your test suite. The problem that I encountered time and time again was when you mocked an object to pass into one test, then you wanted to actually test the REAL version of the object you’d previously mocked or stubbed, you couldn’t. PHP would throw an error:

Code.php:

<?php
    class badger {
    }
    class badger {
    }

%> php code.php
Fatal error: Cannot redeclare class badger

See? It’s a predicament, especially when you want to do some unit testing.

<?php
class weather {
    /**
     * @param string $postcode
     * @return object
     */
    public function getWeatherFromPostcode($postcode) {
        //.. connect to a web service
        $weather = new ThirdPartyWeatherService();
        return $weather->getWeatherAt($postcode);
    }

}

class stuff {

    public function isItRaining($postcode)
    {
        return (
                    $this->weather->getWeatherFromPostcode($postcode)
                          ->precipitation = 'RAIN'
                        ? TRUE
                        : FALSE
               );
    }

    public function injectWeatherObject(weather $o) {
        $this->weather = $o;
    }
}

class doTest extends PHPUnit_Test_Case {

    public function testIsItRaining() {
        $stuff = new stuff();
        $stuff->injectWeatherObject(new weather());
        $this->assertTrue($stuff->isItRaining('RH2 9SS'));
    }
}

The problem with the above code is that, the test may pass or false randomly depending on the weather. Clearly, this is a nightmare! No one thinks that there code is going to behave differently depending on the weather, that’s CRAZY TALK MAN! Well, this code would, especially if the weather is as strange as it is in Reigate.

So, how to get round this: dependency injection and mock objects. See, we had the foresight to inject the weather object into the stuff object. That means we’re halfway there (if you’d just instantiated the weather object in the stuff object, this would be much harder). To workout the solution to this, we need to ask a question: What are we testing?

The answer is, we’re testing the stuff class and, more specifically, the isItRaining() method. So, what do we need to do to test that? Well, for a start, we don’t actually need the full weather object (which might connect to a 3rd party service, which in turn, might be down, or slow or whatever – meaning the test might fail NOT because the unit is broken, but because a dependency is…), we only need to AN object with a ‘precipitation’ property. So, this means we can mock it up. Woo!

Code.php:

class weather {
    /**
     * @param string $postcode
     * @return object
     */
    public function getWeatherFromPostcode($postcode) {
        $w = new stdClass;
        $w->precipitation = 'RAIN'
        return $w;
    }

}

class stuff {

    public function isItRaining($postcode)
    {
        return (
                    $this->weather->getWeatherFromPostcode($postcode)
                          ->precipitation = 'RAIN'
                        ? TRUE
                        : FALSE
               );
    }

    public function injectWeatherObject(weather $o) {
        $this->weather = $o;
    }
}

class doTest extends PHPUnit_Test_Case {

    public function testIsItRaining() {
        $stuff = new stuff();
        $stuff->injectWeatherObject(new weather());
        $this->assertTrue($stuff->isItRaining('RH2 9SS'));
    }
}

Now, we know that it’ll ALWAYS be raining in Reigate (which sucks, but well, what can you do? All in the name of great testing, right?). This test will now pass, unless someone changes isItRaining() and it breaks. But that’s what unit testing is for.

Huzzah, let’s party. Except, we’ve introduced a problem here; what happens if we now weant to test a method in the REAL weather object:

Code.php


class weather {

    public function getTypesOfCloud($type)
    {

        $clouds = array
        (
            'high' =>
                array('Cirrus', 'Cirrocumulus', 'Cirrostratus')
            'medium' =>
                array('Altostratus', 'Altocumulus',)
            'low' =>
                array('Stratocumulus', 'Stratus',)
        );
        if (isset($clouds[$type])) {
            return $clouds[$type];
        }
        else {
            return FALSE;
        }

    }
    /**
     * @param string $postcode
     * @return object
     */
    public function getWeatherFromPostcode($postcode) {
        ..
    }
}
class weather {
    /**
     * @param string $postcode
     * @return object
     */
    public function getWeatherFromPostcode($postcode) {
        $w = new stdClass;
        $w->precipitation = 'RAIN'
        return $w;
    }

}

class stuff {

    public function isItRaining($postcode){ ... }

    public function injectWeatherObject(weather $o) { ... }
}

class doTest extends PHPUnit_Test_Case {

    public function testIsItRaining() { ... }

    public function testGetClouds()
    {
        $weather = new weather();
        $this->assertEquals(
            array('Cirrus', 'Cirrocumulus', 'Cirrostratus'),
            $weather->getTypesOfClouds('high')
        )
}

%> php code.php
Fatal error: Cannot redeclare class weather

OMFG! Now we’ve got two classes called the same thing, this makes it impossible to TEST THEM OMG NOO!

I should mention here that you’ll probably not have all your tests in one file, they’ll probably be split across multiple files and directories. They’re all in one file here to make things easier. However, this doesn’t mean you won’t get the same problem.

The easier thing to do would be to rename the mock weather object to something like ‘weatherMock’, but we can’t because our stuff class expects the object that is injected to be an instance of the ‘weather’ class.

“Well, remove the typehinting dumbass!” I hear you cry, throwing your hands in the air.

I could, but I shan’t. Because I WANT the typehinting there, it’s a conract and I am BOUND by it. I must have a weather object passed in, I can’t have ANY OLD object being passed in now can I? What would happen if someone passed in a ‘cheese’ object? DOES CHEESE HAVE PRECIPITATION?

Anyway, this is how I chose to solve this problem:

Code.php

<?
namespace Weather {
    require_once 'PHPUnit/Framework.php';
    abstract class abstractWeather {
        public function getTypesOfCloud() {}
        public function getWeatherFromPostcode() {}
    }

    class weather extends abstractWeather {

        public function getTypesOfCloud($type)
        {

            $clouds = array
            (
                'high' =>
                    array('Cirrus', 'Cirrocumulus', 'Cirrostratus'),
                'medium' =>
                    array('Altostratus', 'Altocumulus'),
                'low' =>
                    array('Stratocumulus', 'Stratus'),
            );
            if (isset($clouds[$type])) {
                return $clouds[$type];
            }
            else {
                return FALSE;
            }

        }
        /**
         * @param string $postcode
         * @return object
         */
        public function getWeatherFromPostcode($postcode) {
            // Commented as this doesn't actually exist.
            //$weather = new ThirdPartyWeatherService();
            //return $weather->getWeatherAt($postcode);
        }
    }
}
namespace WeatherMock {

    class weather extends \Weather\abstractWeather {

        /**
         * @param string $postcode
         * @return object
         */
        public function getWeatherFromPostcode($postcode) {
            $w = new \stdClass;
            $w->precipitation = 'RAIN';
            return $w;
        }

    }

}

namespace Main {
    class stuff {

        public function IsItRaining($postcode) {
            return (
                        $this->weather->getWeatherFromPostcode($postcode)
                                      ->precipitation = 'RAIN'
                            ? TRUE
                            : FALSE
            );
        }

        public function injectWeatherObject(\Weather\abstractWeather $o) {
            $this->weather = $o;
        }
    }

    class doTest extends \PHPUnit_Framework_TestCase {

        public function testIsItRaining() {
            $stuff = new stuff();
            $stuff->injectWeatherObject(new \WeatherMock\weather());
            $this->assertTrue($stuff->isItRaining('RH2 9SS'));
        }

        public function testGetClouds()
        {
            $weather = new \Weather\weather();
            $this->assertEquals(
                array('Cirrus', 'Cirrocumulus', 'Cirrostratus'),
                $weather->getTypesOfCloud('high')
            );
        }
    }
}

So, the solution is two fold.

  1. I made both the weather classes (real and mock) an extension of the abstractWeather class. This is good, because it means my contract is still in place (if a little more flexible) and I can force people to use the methods prescribed.
  2. I wrapped the abstract class and the real object in a namespace and the mock weather object in another namespace. This means I can have two instances of a class named ‘weather’ as they reside in different namespaces. I could have named the mock weather object as ‘weatherMock’ and not had the namespaces, but adding them makes it much cleaner. If I know I’m using namespaces and not going to get into trouble with conflicting error names, I can be confident that, with a HUGE library of tests, when they’re all run together, I’m not going to get the error (I don’t know if a colleague created a weatherMock() class two days ago for a different part of the application, but for use in the same suite of tests – using namespaces means it doesn’t matter).

Now, you could use interfaces instead of abstract classes, but I prefer the abtract classes as they give you something concrete to work from and, really, if you’re wanting to ride modular, encapsulated code, then you SHOULD be using abstract classes.

The only other thing to remember (which caught me out) is that stdClass lives in the global space, so, whenever instantiating a new stdClass, it must be pre-pended with a forward slase: \stdClass (the same goes for anything that ISN’T in a namespace when you’re working in one: \PHPUnit_Framework_TestCase

Feel free to copy this code and run it in your own environment, it *should* run!

This is not the only, or definitive method of achieving this. But I think it fits my ideal of having an elegant solution and it means the actual code (and not the tests) isn’t mauled about JUST to make the tests fit. Which is where a lot of unit testing falls down.

If you have another method or other ideas for how to get round dependcy and classname conflicts, then I’d love to hear them. Please post in the comments below!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s