How I'm Writing Unit / Functional Tests

So...testing.

That thing that everyone says is so important but you don't really learn about it in school.

I've had some trials and tribulations with testing so I'm going to just dump out some thoughts here.

Background

I first started testing in PHP, building a fairly large distributed platform. We had several API's talking to each other, backed by a MySQL database, running on the CodeIgniter framework.

My job was all over the place. I was running the tech show, working with clients, building spec with shareholders, leading project management. A bit of a one man (college kid) juggling act.

The worst was talking to customers.

I have sympathy for 'em. I love delivering to them. Hate talking to them. Particularly hate talking to them when they are calling me up asking why they can't use x feature or saying that y feature is broken. Was part of my responsiblity, so I just rolled with the punches.

After several regressions and several heated phone calls, enough was enough.

I decided that testing would solve everything!

Enter the Unit Tests!

First and foremost, I had to wrestle with CodeIgniter to get some sort of testing framework setup. For those of you who aren't familiar with CodeIgniter- it's an older PHP framework that was built for PHP4 support and never really modernized for PHP5. It also has some interesting scoping things where the entire application is a huge object container and loading mechanism, so you end up doing things like:

$this->load->library('PaymentProcessor', $params);

It's a bit wonky but I'm not going to get into it.

All you have to know is that unit testing wasn't baked in. Had to devise some trickery. I ended up using this guy (which saved my life, thank you Kenjis whoemever you may be):

https://bitbucket.org/kenjis/my-ciunit

So boom and boom, I had my unit tests going, support for database fixtures. Not bad. Until, of course, you start testing things that hit external API's or systems.

Seeding a database for each test, no problem. That was easy. But what about making a call to a payment processor? Or your own internal API? A search engine?

Before I had the time to solve those problems at that job, I was off to my next gig, finding myself in a bit of dependency hell.

Dependencies

I found myself working in a custom PHP framework chop full of lovely things including home baked ORM and persistence layer. During the interviewing process, I was pretty damn confident that I'd be able to bring my prior testing experience along with me. We even had a strong mutual agreement to introduce a testing culture at this particular shop.

There was a bit of a problem when I started though...

Everything was static.

Database calls, filesystem calls, HTTP request/responses, third party API calls. Stuff like this:

$customer = Customer::find($id);
$order = new Order;

// we had generic add functions that did different things based on types.
$customer->add($order);
$order->place();

FraudAPI::send($order);
Logger::log('Sent an order for fraud checking.');

// send to an internal API
HTTP::post('/some/api', ['order_id => $order->id]);

Keep in mind these aren't magic static calls to implementations like the facades in Laravel. Just a bunch of class with a bunch of state issues.

On top that there wasn't any thought about dependency injection. Developers would just throw new objects into any old function. No evident awareness of scope or state to be found.

So I pulled my sleeves up and dug in trying to bring testing to this codebase.

Initially, I was a bit confused. CodeIgniter made it easy, since everything wrapped around a global container you could sorta mock out classes if you used their loading syntax: $this->load->model()

In this case however, there was no way to mock anything. Not with tons of object instantation scattered all over the place.

MOCK EVERYTHING

Since there wasn't a testing database to be found, my first step was to make mocks for everything. Every database call had to mocked. This meant we needed some concrete instances, being injected into constructors.

After a few days of refactoring, I found myself with some classes that looked like this:

class PaymentProcessor {

    public function __construct(
        PaymentGateway $gateway,
        OrderRepository $order,
        OrderCommentor $order_comments,
        InvoiceRepository $invoice,
        Database $database,    // some queries were hard coded
        Logger $logger
    ) {
        // wahoo!
    }
}

Naturally my tests would resemble something like this:

public function testItDoesSomething() {
    $payment_gateway_mock = Mockery::mock('PaymentGateway');
    $order_repository_mock = Mockery::mock('OrderRepository');
    $order_commentor_mock = Mockery::mock('OrderCommentor');
    // and so on...

    $payment_gateway_mock->shouldReceieve('authorize')->once();
    $order_repository_mock->shouldReceive('update')->once();
    $order_commentor_mock->shouldReceive('make')->once();
    // and so on...

    $class = new PaymentProcessor(
        $payment_gateway_mock,
        $order_repository_mock,
        $order_commentor_mock,
        // and so on...
    );
}

This is actually a pretty tame example. Refactoring code that has never been designed with dependencies or SOLID principles in mind means each function has its own set of dependencies.

A lot of these testing issues stemmed from bad design, but part of being an engineer is working under the constraints you are dealt with.

My tests resulting were long, complicated, even more complex that the actual code I was writing.

Stop mocking me!

When maintaing your tests becomes a hassle, you done goofed.

I eventually gave up trying to mock every single thing and wrote a testing database, complete with seeds and fixtures so each test could have its own data. This meant I could at least stop mocking all those database calls and focus on the service / model layers.

Huzzah!

Now I had some code that looked like this:

class OrderRescheduler {

    public function __construct(
        OrderRepository $order = null,
        OrderCommentor $comments = null,
        OrderFraudAPI $api = null
    ) {
        $this->order_repository = $order ?: new OrderRepository;
        $this->order_commentor = $comments ?: new OrderCommentor;
        $this->order_fraud = $api :? new OrderFraudAPI;
    }
}

With tests like:

// Before this test, we have database fixtures that populate any 
// tables that are related to this test. After the test is run, we
// remove all the data to return to a clean slate.
public function testOrderReschedulingSendsFraudCheck() {
    $mock = Mockery::mock('OrderFraudAPI');
    $mock->shouldReceive('check')->once();

    $rescheduler = new OrderScheduler(null, null, $mock);
}

A little bit better!

We would only mock what we need, making all the constructor parameters of the OrderRescheduler class optional with sane defaults.

Tests become easy! Just pass in the parameter you need, null for everything else.

I came to a bit of a problem with this approach though. Having the optional dependencies led to ambiguity and misuse. We found that this code was a bit brittle, a lot of bugs were creeping out.

Take a look at this snippet:

// the interface
interface SomethingInterface {
    public function doSomething();
}

// the implementation
class SomeClass {

   public function __construct(SomethingInterface $something = null) {
       $this->something = $something ?: new DefaultSomething;
   }

   public function do() {
       return $this->something->doSomething();
   }
}

// the test
class SomeClassTest {

    public function testItShouldDoSomething() {
        $something = new SomeClass;

        // calls doSomething() on an instance of DefaultSomething
        $something->do();

        $something = new SomeClass(new AwesomeSomething);

        // calls doSomething() on an instance of AwesomeSomething
        $something->do();
    }
}

Why leave a class dependency to fate? Isn't it a bit better to just concretely say what you are injecting no matter what?

After seeing some code like this, I could tell that I was designing classes just so you can test them. Not the best way to approach things. Your methods and objects will start to loose their credibility, becoming a bit disconnected with reality. The end user will be bewildered by your API as it was formed to be used by a unit test.

Not cool!

By now I had realized that the end-user of your classes needed methods that made sense, constructors that worked better. We want our objects to be realistic reflections of whats real. It helps us wire together all these wild abstractions.

Enter: the functional test!

The Functional Test

A functional test focuses on a slice of your system, typically as an end-user would see it.

When we write code on a team, our users are the other folks in the trenches using our objects.

Designing my classes just so they could be tested resulted in some really complicated, verbose code. I tended to focus on dependencies more than anything so my classes had massive constructors. I would try to over-abstract, writing a lot of interfaces I didn't need just so we could replace implementations for the sake of testing.

I decided that this wouldn't cut it, we needed an approach to desigining classes that made sense while we could put under test coverage.

The solution was to write your tests with code used exactly as they would be by another developer.

Doing so tightened up my API's

Accomplishing this was pretty straightforward, there's three rules:

  • Never mock database calls.
  • Only mock calls that reach outside the current system.
  • Optionally mock the local filesystem.

All we needed was a test database with an easy way to seed data (see http://laravel.com/docs/4.2/migrations). Each test will load up what it needs and then clear any relevent tables after its done.

As far as mocking goes, we just need to replace anything hitting a third party API or a system process.

The resulting tests look a lot better.

public function setUp() {
    parent::setUp();

    // App::make() - we are using a container to hold bindings for objects
    // that we use. This allows us to re-use the same configurations so that
    // an object is always made how you define it. Useful for stuff like
    // database connections, complicated services.
    //
    // see: http://laravel.com/docs/4.2/ioc
    //
    $this->order_repository = new OrderRepository(App::make('database'));
    $this->order_commentor = new OrderCommentor(App::make('database'));
}

public function testOrderReschedulingSendsFraudCheck() {
    $mock = Mockery::mock('OrderFraudAPI');
    $mock->shouldReceive('check')->once();

    $rescheduler = new OrderScheduler($this->order_repository, $this->order_commentor, $mock);

    $rescheduler->reschedule(10001);
}

Notice how there was minimal setup, we made our object without any optional dependencies, and we called it just as it would have been as part of real code.

Wrapping it up

So...testing. This was a bit of a war stories / brain dump, but it gives an idea about how I got to where I am in writing tests and classes. I like doing tests like this because:

  • I don't like optional dependencies in my classes.
  • I don't want to be mocking every database call.
  • I like my classes to have as few dependencies as possible.
  • Getting a test database / fixtures is cheap and easy.
  • I want my tests to be concise.
  • I want my tests to be very close to how objects used are in reality.

Hopefully, you found something interesting in all this. I'm sure I'll have plenty more on testing in the future.

Thanks.

Further Reading

comments powered by Disqus