Behavioral Driven Unit Testing

Wikipedia has a great definition of unit testing.

unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures are tested to determine if they are fit for use.

In other words: unit testing is about testing the smalls bits (units!!) of your application to make sure they work in isolation.

That Doesn't Really Say Much

The wikipedia definition above is wonderful. To someone who is a more experienced test writer and user, it might even be insightful.

But it doesn't say much about how to actually write unit tests.

Let's consider the parts of pretty much every test case (be it a unit, integration, or acceptance test):

  1. Setup: creating objects, setting up mocks, etc
  2. Action: calling the method/object under test
  3. Verification: making sure results are what you expect, verifying mocks were called, etc.

It's About Behavior

Writing tests, you don't really care about the underlying implementation all that much. You're really trying to answer a single question:

Given the environment, state, and inputs does the object under test give the expected output?

In other words: does my object, function, or method behave as I expect?

In the list above, the important bits for testing behavior are numbers two and three. Two is actually causing the object to behave in some way. Three is making sure the object behaved properly.

How to (Really) Write Unit Tests

Let's say you have an object -- or even just a mental plan of an object. That object does (or will do) something.

You already know that behavior. Start with just a test name that adaquately explains the behvaior.

<?php
class SomeTest
{
    public function testThatABehaviorHappensAsExpected()
    {

    }
}

Now, instead of diving into the complicated setup steps, start with the actions and verifications.

<?php
class SomeTest
{
    public function testThatABehaviorHappensAsExpected()
    {
        // TODO setup

        // Actions
        $result = $objectUnderTest->work();

        // verification
        Assert::assertEquals(42, $result);
    }
}

Now back up: what sort of environment, state, and objects need to be setup? How should the $objectUnderTest's collaborators act when their methods are called? Do you need mock objects? Can you use stubs or some other test double or should you just use real objects?

With the actual actions in place, the setup should become much more clear.

Then you write a few more tests, and you see that some parts of the setup, like constructing the object under test itself and maybe creating mocks for its collaborators happen a lot. So you move those to a method that runs before every test and keep your tests DRY.

But Working Like This Won't Cover Every Code Path?!

Remember that an object's behavior depends on its surrounding environment and state (at least in functions and methods that aren't pure).

If a collaborator throwing an exception is dealt with by the object, that's part of the object's behavior. It's just less apparent.

Once you've done some happy path scenarios, start looking deeper into what paths of the code in question aren't covered. What sorts of behavior does the object have on those paths? Can you describe it?

Repeat the process above: write a test name that adequately describes the behavior in question then write the actions and verifications for that behavior and fill in the setup.

More Maintainable Tests

The best side effect of working as described above is that your tests will probably grow much better and be less of a burden.

An object's behavior is probably not going to radically change over the course of an application's life. The action and verification parts of your tests can likely remain largely the same. The setup might change and evolve as time goes on, but the core behavior and test names will continue to be relevant.

#