TDD in a self-experiment

Veröffentlicht am 26. Juni 2008 (vor 1421 Tagen)

I had a chat about test-driven development with PHP recently. It seems that TDD is not widely used in PHP, so I felt that I a little self-experiment was in order. After all, you want to know what you are talking about.

A while back, I had come up with an idea on how to make that small CMS I use on my own web sites more flexible. I wanted to see how I could translate that idea to code using TDD. And I made a very interesting experience.

The CMS I am using is a small engine that puts together (potentially multi-lingual) page content, templates, and a site structure, and creates semi-static pages. It does not have a sleek GUI frontend, because by nature I am not afraid of a text editor, and most of the time get quicker results by just writing HTML than fighting with one of these what-you-see-is-what-you-might-get HTML editors.

However, I always felt that that CMS engine did not give me enough flexibility, so here I came up with a new concept of putting together content blocks. The idea is to define a template that contains markers (nothing new here), I call them slots, and to assign content blocks to these slots. Each a content block can be static content, or custom PHP code, allowing for maximum flexiblity.

The core of that new CMS engine would be one configuration file. My original notes looked something like this:

*: top_menu = top_menu
*: body = empty
*: foot = copyright

index.php: body = welcome
some/dir: body = something_else

Each *: defines the basic mapping which can be overridden for individual pages, or whole directories, including their subdirectories. I guess this approach is inspired by the YAML configuration files that Symfony uses.

When I got back to the idea a while later, I realized that I would also want to be able to define a template. Also, I tried to simplify the configuration file syntax. My second notes looked something like this:

template = bla
title_slot = page_title
copyright_slot = copyright

products/
copyright_slot = ...

I started thinking about an implementation. Obviously, I would need a kind of "best matching pattern" algorithm. I thought of Path objects that have one Template and many Rule objects associated with them. My idea was to create a tree of Path objects and traverse this tree to find the appropriate slot/block mapping and template for a given URL. I was planning to create a ContentMapper object that would actually do the work, returning the slot/block mapping and template for a given URL.

This concept seemed to be an interesting testbed for some test-driven PHP development. I fired up my Laptop and installed the latest version of PHPUnit. Now, TDD is about starting with the tests, and I must admit that I started off by violating this rule. I started off by creating the ContentMapper, Rule, and Url class. Since a Template only consists of one name, I figured that I would not create an object, but make that template name a member of Url, which I figured would be a more intuitive class name than Path.

I implemented the basic relationships between the objects (Url has children and is associated to many Rule objects, one for each content slot), and started to think about writing tests. My first test was to set up a site structure, define a template, then assert that the ContentMapper would actually return the template name when queried for a template. This was my first nice TDD experience: Instead of writing a major part of the matching algorithm, then figuring out test cases, I just implemented the special case when just one default template is defined. This is, obviously, very simple: just return that one template name for any URL. Though I had not really accomplished a lot, I found it interesting to actually start off by a more edgy case rather than the real algorithm.

My second test case was to build a more complex strucure: a default template is overridden for a subdirectory, so when retrieving the template for an URL below that subdirectory, you’d expect to get that template's name, while the default (empty) URL still must return the default template name. It seemed not too difficult to implement this, once the test was in place: traverse the tree of Url objects to find the best matching template for the given URL. Iterators came to mind. I had some trouble to get recursive iteration to work, because the documentation is not too clear about what that getChildren() method should return. I found some sample code in ezComponents, but did not get things to work properly.

After a while I decided to just not use Iterators. Thinking along the lines of "what is the simplest thing that could possibly work", I started questioning myself wether Rule objects were required at all: basically, they just contain a key/value pair. This in turn lead me to the question of how much effort it would actually be to build an ordered tree of Url objects. The original idea was to traverse the tree until the first longer URL is found, and then return the last found template or slot/block mapping. This only works when the tree is sorted. Would I have to make the tree self-sorting, or rely on the ContentMapper to insert each Url object at the right place in the tree?

This is the point where I sensed a bad smell in my code. There seemed to be too much effort involved in that whole concept that did not really contribute to the actual solution. I decided to start over, and use real TDD this time.

I reused my test that would define a default template, and assert that this template be returned for every URL. My new ContentMapper object just keeps an associative array of URL => TemplateName pairs. This array I can keep sorted by just calling ksort() after adding another element. I quickly got the existing test to pass, and started to add more tests like, for example, this one:

public function testSubTemplateForNonmatchingUrl()
{
    $cm = new enoContentMapper();
    $cm->setTemplate('template.php');
    $cm->setTemplate('sub.php', 'some/');
    $this->assertEquals('sub.php', $cm->getTemplate('some/url.php'));
    $this->assertEquals('sub.php', $cm->getTemplate('some/sub/url.php'));
}

This test ensures that defining a template for a certain subdirectory works. The code of my getTemplate() method is just 22 lines long, and I use a lot of whitespace for formatting:

public function getTemplate($aUrl = '')
{
    $best_match = $this->templates[''];
    foreach (array_keys($this->templates) as $key) {
        if ($key == ''&& $aUrl == '')
        {
            return $this->templates[$key];
        }

        if ($key == $aUrl)
        {
            return $this->templates[$key];
        }

        if ($key != '' && $key == substr($aUrl, 0, strlen($key)))
        {
            $best_match = $this->templates[$key];
        }
    }

    return $best_match;
}

This definitely is much easier than putting together Path, Rule, and Template objects, making sure the tree is in order, and then iterate over the tree to find the best matching rule or template. It is still OOP, and can easily be tested.

The TDD approach has taught me that by writing the test first against a simple API, then thinking about how to implement a feature, you can definitely become a much more productive programmer, since you waste less time on building infrastructure that you may not really need.

Now, in real life you want a configuration file. Again, TDD and "the PHP way" held a nice 15-line solution in store for me. I had the idea to try and use parse_ini_file() instead of dealing with XML or YAML files.

So, first of all, I drew up an ini file:

[/]

template = template.php

copyright_slot = copyright_block
footer_slot = footer_block

[sub/]

template = sub.php
copyright_slot = sub_copyright_block
footer_slot = sub_footer_block

[sub/url.php]

template = special.php
copyright_slot = special_copyright_block
footer_slot = special_footer_block

Everything I need seems to be in the ini file, so I wrote a test that would ensure that the ini file has been correctly parsed:

class ParseTest extends PHPUnit_Framework_TestCase
{
    public function testTemplates()
    {
        $cm = new enoContentMapper('../testdata/default');
        $this->assertEquals('template.php', $cm->getTemplate());
        $this->assertEquals('special.php', $cm->getTemplate('sub/url.php'));
        $this->assertEquals('template.php', $cm->getTemplate('some/sub/url.php'));
        $this->assertEquals('sub.php', $cm->getTemplate('sub/sub/url.php'));
    }

    public function testBlocks()
    {
        $cm = new enoContentMapper('../testdata/default');
        $this->assertEquals('copyright_block', $cm->getBlock('copyright_slot'));
        $this->assertEquals('footer_block', $cm->getBlock('footer_slot'));
        $this->assertEquals('sub_copyright_block', $cm->getBlock('copyright_slot', 'sub/something.php'));
        $this->assertEquals('sub_footer_block', $cm->getBlock('footer_slot', 'sub/something.php'));
        $this->assertEquals('special_copyright_block', $cm->getBlock('copyright_slot', 'sub/url.php'));
        $this->assertEquals('special_footer_block', $cm->getBlock('footer_slot', 'sub/url.php'));
        $this->assertEquals('sub_copyright_block', $cm->getBlock('copyright_slot', 'sub/sub/url.php'));
        $this->assertEquals('sub_footer_block', $cm->getBlock('footer_slot', 'sub/sub/url.php'));
    }
}

Then, I wrote a parseIniFile() method for my class. This method loops over the array that parse_ini_file() returns and calls setTemplate() and setBlock() methods to build the mapping structure. Here is the method:

protected function parseIniFile($aFilename)
{
    foreach (parse_ini_file($aFilename, true) as $url => $array)
    {
        if (isset($array['template']))
        {
            $this->setTemplate($array['template'], $url);
            unset($array['template']);
        }

        foreach ($array as $slot => $block)
        {
            $this->setBlock($slot, $block, $url);
        }
    }
}

I ran the test, and it passed. Can it get any simpler? My ContentMapper class is self-contained, has 108 lines (again, with a lot of whitespace in between). It does contain no error handling whatsoever at this point, but it will not be a big deal to add that.

It seems that TDD helps you to keep things simple. While I had started off with a much more complex implementation in mind, I learned that just a few lines of PHP code are sufficient to solve my problem. The API of my ContentMapper class is as simple as it can get: it has get and set accessor methods for templates and blocks, allowing you to define a mapping rules, and then query the object. I now also have three test classes for this object, allowing me to test template mapping, block mapping, and ini parsing. I will definitely give TDD another shot in the future.

I am also curious to hear about your TDD experiences. Write to me at or add a comment to this blog post.

Tags: PHP, TDD, CMS

Nächster Eintrag »»»

Kommentare?

Kommentare bitte per E-Mail.

Stefan Priebsch

Stefan Priebsch

Diplom-Informatiker Stefan Priebsch ist IT-Consultant, Trainer und Spezialist für die Entwicklung PHP-basierter Software.

Er ist Autor zahlreicher Bücher und Fachartikel über verschiedene Aspekte des Software-Lebenszyklus und spricht regelmäßig auf internationalen IT-Konferenzen.

Stefan Priebsch ist verheiratet und Vater von Zwillingen. Er lebt und arbeitet in Wolfratshausen bei München und ist Mitgründer und Principal Consultant von thePHP.cc.

"I had the pleasure of working with Stefan on a 2 part PHP Webinar series. Stefan is very knowledgable, and presented an excellent Webinar. Very professional, and very pleasant to work with."
--Nili Gafni, Zend Technologies, Inc.

Blog

Der Weg zur Tanzfläche

Es wurde schon viel darüber spekuliert, wie groß der Anteil der PHP-Nutzer ist, die von HipHop profitieren werden. Die Zahlen bewegen sich im Wesentlichen zwischen 0% und 100%, daher versuche ich mich gar nicht erst an einer eigenen Schätzung, sondern will stattdessen über die Voraussetzungen schreiben, die ein Team beziehungsweise eine Firma erfüllen muss, um von HipHop profitieren zu können. (vor 826 Tagen)
» Blog-Eintrag lesen

Before you can dance

A lot has already be speculated about what percentage of existing PHP users would benefit from using HipHop. The numbers seem to range somewhere between 0% and 100%. I will not try a guess myself, but rather try to shed some light on some the most important prerequisites I think there are for a company to benefit from HipHop. (vor 834 Tagen)
» Blog-Eintrag lesen

Der erste deutsche PHP Summit

Der erste deutsche PHP Summit - powered by thePHP.cc - ist eine neue und einzigartige Veranstaltung, die alle wichtigen Themen rund um die Software-Entwicklung mit PHP in kompakter Form vermittelt. (vor 836 Tagen)
» Blog-Eintrag lesen

CodeWorks09 Derick Rethans Canada CRAP Migration Slideshare tek09 Relaunch Presentation Atlanta Book OOP Conference Magento Slides MVC Workshop Selenium Facebook PHP Quebec 09 Chicago hack Feedback Testing U.S.A. Webcast HipHop Arne Blankerts fail Training thePHP.cc PHP 5.3 C++ MTA Sebastian Bergmann Montreal Blog PHP Lifecycle Wordpress

Twitter

RT @thePHPcc: 18 PHP Power Workshops in 3 Tagen? Klar geht das! Beim PHP Summit 2010 - http://phpsummit.de - Schnell buchen, bevor es zu ... (vor 837 Tagen)
» Tweet lesen

@s_bergmann Something must be wrong. There is no water ;-) (vor 837 Tagen)
» Tweet lesen

New blog post: http://priebsch.de/blog/how-to-turn-bad-code-into-good-code/ (vor 838 Tagen)
» Tweet lesen

» Auf Twitter folgen