Skip to content

Instantly share code, notes, and snippets.

@nackjicholson
Last active August 29, 2015 14:11
Show Gist options
  • Save nackjicholson/94c1a86e977cc20a3632 to your computer and use it in GitHub Desktop.
Save nackjicholson/94c1a86e977cc20a3632 to your computer and use it in GitHub Desktop.
How I let TDD happen in PHP

Test Driven Development is a practiced skill. I'll be the first to tell you, I don't have it. I am working on it though. What I want to share in this blog is the process I go through to flip my brain from thinking about how to write code, to thinking about how to write tests I can code against.

I'm going to assume if you've found this blog you already know what TDD is, why you should be doing it, the basics of PHPUnit, and you don't need a lengthy description of what we're talking about here. The gist is this:

1. Write unit tests
2. Write code that passes those tests.
3. Refactor without failing the tests.

The Task

Before you start you should know what you want to do. Writing tests first will force you to make a plan. In the case of the example for this blog -- I had data coming back from a message queue service in the form of an associative array, and I wanted to put the message information into a Data Transfer Object (DTO). You can think of a DTO as a bag of data as a class. In php having data structured in a class means you can leverage type hinting and checking to do more with it than you can with an associative array. That's more than I'll go into now, but just know, I had like totally really important reasons to do this job:

[
    'body' => '{"foobar": "JSON message!"}',
    'receiptHandle' => 'abc123',
    // ...
    // And many other properties I don't need
]

INTO

class Message
{
    public $body = '{"foobar": "JSON message!"}';
    public $receiptHandle = 'abc123';
}

I'm going to call my thing that does this job a MessageHydrator, because...someone came up with that name. It's a thing in OO design. If you need a metaphor, I got you! Data is water and class rhymes with glass, and if you put data in a class it is hydrated.

So I need a test which will prove that my MessageHydrator class can take an associative array and turn it into a Message.

namespace Cascade\Sensei\Data\Message;

class MessageHydratorTest extends \PHPUnit_Framework_TestCase
{
    /** @var MessageHydrator */
    private $hydrator;

    public function setUp()
    {
        $this->hydrator = new MessageHydrator();
    }

    public function testItShouldHydrateAMessageFromSourceData()
    {
        $message = new Message();
        $sourceData = ['body' => 'foo', 'receiptHandle' => 'bar'];

        $this->hydrator->hydrate($sourceData, $message);

        $expectedMessage = new Message();
        $expectedMessage->body = 'foo';
        $expectedMessage->receiptHandle = 'bar';

        $this->assertEquals($expectedMessage, $message);
    }
}

Okay, that fails. I need to write a class that passes it. Minimally! Don't get all fancy and do the correct things you know you need to do, just pass the test.

I'm going to take keys and put them on a Message. That's it!

namespace Cascade\Sensei\Data\Message;

class MessageHydrator
{
    public function hydrate($sourceData, Message $message)
    {
        $message->body = $sourceData['body'];
        $message->receiptHandle = $sourceData['receiptHandle'];
    }
}

Bingo!

Green. But, that can't be enough right? Now I stop and think about things for a second. This doesn't really meet the requirements of my plan -- remember all that data I need to ignore? I should make sure that works.

public function testItShouldHydrateAMessageFromSourceData()
{
    $message = new Message();

    $sourceData = [
        'body' => 'foo',
        'receiptHandle' => 'bar',
        // Just gonna add one more thing here
        'doNotWant' => 'noThanks'
    ];

    $this->hydrator->hydrate($sourceData, $message);

    $expectedMessage = new Message();
    $expectedMessage->body = 'foo';
    $expectedMessage->receiptHandle = 'bar';

    $this->assertEquals($expectedMessage, $message);
}

Oh cool, tests are still green. I'm done, this code is perfect.

And honestly I could be done, this code works. But there is that step 3, the pesky refactoring part. Now is the time for reflection, and deep honesty with yourself about how bad you are at this. Ask could this be better?

Yes.

It would be nice if I didn't hardcode every single property translation. With a small list like this it's not a big deal, but let's say a week from now (and every week after that) it becomes necessary to add some other data attribute out of the associative array, what do I do then? This is software folks, change is going to happen. I add a property Message::property, and another line of code in the hydrator $message->property = $sourceData['property'];. Pretty soon I have a maintenance headache in the hydrator class. It would be nice if I just added my properties to the Message class, and the hydrator could just know that if the property is in the class, I should take it out of the array and put it there. This is a job for a loop!

Let's refactor:

class MessageHydrator
{
    public function hydrate($sourceData, Message $message)
    {
        foreach ($sourceData as $property => $value) {
            $message->$property = $value;
        }
    }
}

Oh god, that fails. Abort the mission! No...maintainability is worth it.

At this point the test will output this: Failed asserting that two objects are equal.

Okay, to really see why those objects aren't equal I step through debug the code on the line which is failing and I find that the Message has a property Message::doNotWant equal to noThanks. Oops, we need to skip properties that aren't defined in the Message class. PHP has a function property_exists we can use to check, and just continue to the next property.

class MessageHydrator
{
    public function hydrate($sourceData, Message $message)
    {
        foreach ($sourceData as $property => $value) {
            if (!property_exists($message, $property)) {
                continue;
            }

            $message->$property = $value;
        }
    }
}

Bango!

Tests are green.

But the famed Portland Trailblazers radio announcer Bill Schonley had a catchphrase "Bingo! Bango! Bongo!". I don't know what it meant, but it sounded great on the radio. We only have "Bingo! Bango!" and it just doesn't have that same ring.

I just want to test a few more things.

What if I under load the array by omitting the body key? I should get a message that has a receipt, but body is null. Write the test.

public function testItShouldIgnorePropertiesAbsentFromSourceData()
{
    // Intentionally leaving out the body.
    $sourceData = ['receiptHandle' => 'bar'];

    $expectedMessage = new Message();
    $expectedMessage->receiptHandle = 'bar';

    $message = new Message();
    $this->hydrator->hydrate($sourceData, $message);

    $this->assertEquals($expectedMessage, $message);
    $this->assertNull($message->body);
}

What if $sourceData wasn't an array at all! What if it were a stdClass with properties. It could happen, and I think this code would work for it.

public function testItShouldHydrateDtoFromObject()
{
    $sourceData = new \stdClass();
    $sourceData->body = 'foo';
    $sourceData->receiptHandle = 'bar';
    $sourceData->doNotWant = 'NoThanks';

    $expectedMessage = new Message();
    $expectedMessage->body = 'foo';
    $expectedMessage->receiptHandle = 'bar';

    $message = new Message();
    $this->hydrator->hydrate($sourceData, $message);

    $this->assertEquals($expectedMessage, $message);
}

Bongo!

The tests still pass. This code is done.

Thanks so much for reading. Hope it helps. If you have comments, leave em. If you want to see if I can write shorter things follow me on twitter @nackjicholsonn

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment