testing

New project offshoots

So, in the never ending endeavor to make things as easy as possible for people to work with, I have a new mini project (actually two!).

majaxInstallerPlugin for symfony (only tested on 1.4.x)

and

MajaxInstaller for everyone else!

MajaxInstaller takes an XML configuration and prompts users interactive questions about the files you describe, to get easy configuration details.

The symfony plugin is just designed to be better integrated into symfony, accepting a yaml config file, and adding a majax:install task

There are no phpunit tests yet in the repository for MajaxInstaller, though I have one or two in majaxInstallerPlugin that actually test the base framework.

I look forward to hearing what people think!

Unit Testing with PHPUnit in symfony

Today we’re going to talk about something I know next to nothing about, but am working on.

Recently I came to the realization that it is time to “Code Up” and learn how to be proficient in building tests, and writing testable code. Testing is a discipline that you will be able to cross code boundaries, something you will be able to carry with you to any language, once you pick up the mindset. More importantly than that, testing allows you to verify your code is working without manually testing or worse, just hoping it works.

So now that we’ve touched on the why, let’s talk about how.

If you’re going to do it, do it right!

PHPUnit (by Sebastian Bergmann) is quite literally the gold standard of testing in PHP. It probably helps that Sebastian has taken code metrics in PHP from zero to hero. So it stands to reason that PHPUnit is where you start to look.

A lot of what I write are plugins, because I use things here, there, and everywhere. So one of the most important things to me, was that whatever I used had at least some plugin support. The two plugins that fit what I was looking for closely enough are sfPHPUnitPlugin and sfPHPUnit2Plugin. I chose sfPHPUnit2Plugin simply because I saw it had a github repository, which meant that contributing would be easy, and worst case scenario, keeping my customizations up to date with the core would be a snap.

Note: Just to clarify, both sfPHPUnitPlugin and sfPHPUnit2Plugin work with PHPUnit 3.5.x.

Long story short, I made a pull request on March 11th to add support for plugins holding on to phpunit tests, along with some other goodies. A little while later, it was incorporated! Hurrah! Power to the people. :)

Anyway! For the purpose of this, we’re going to assume you’re using sfPHPUnit2Plugin, as that’s what I use. It’s easy. Install the plugin. Run ./symfony phpunit:generate-config, and then run phpunit. If you already have a phpunit.xml file in your root, copy phpunit.xml.dist over it, as it will be updated with the latest path set.

To make your first unit test, it’s easiest to just use the plugin’s tasks to build them, and work from there. This is a simple deal!

./symfony phpunit:generate-unit MyTestName

That will set up the phpunit basics, and put your unit test in test/phpunit/unit/MyTestNameTest.php. See? EASY!

To run your test, either just run ‘phpunit’, or run ‘phpunit test/phpunit/unit/MyTestNameTest.php’. EASY!

Now you know HOW to test, so let’s look at WHAT to test. Because you can test everything but if you’re not doing it in a way that makes sense, you’re just going to get frustrated, and give up. Or worse, you won’t realize you’re testing the wrong things until disaster strikes.

JMather’s Novice Testing Rules

1. Don’t test things that are already being tested

Doctrine and Propel both have tons of tests to ensure they’re working. You don’t have to confirm that when you set something, it gets set, unless you are overriding the setter. You don’t have to confirm that when you save, it is added to the database unless you are testing a database connection.

2. Test functionality, not objects

The most important thing to test is your business logic. Not every line deserves a test. Focus on complex business logic. Though, likely, you will end up testing objects if you build them to only contain specific domain logic, but short of that, test what matters. It’s ten times better to have 50% code coverage testing all the really important logic than to have 20% code coverage because you’re writing massive amounts of silly tests making sure that a + b = c.

3. Just start testing

Your first tests will be bad. No, scratch that, they will be horrible. Why? Because you’ll be testing code that wasn’t written to be tested. Want an example? Look at this little ditty from majaxDoctrineMediaPlugin. We’ll do two clips of the same function, before and after.

And then the “work in progress”…

The first thing you will notice is that the new one is ~100 lines, as opposed to ~250. It is still quite long, but it has come a long way. The second is that it has been moved into it’s own class. Why? So we can test it! All of the referenced builders (i.e. $this->path_builder, $this->filename_builder and the like) can be swapped out for mock objects with predictable behaviors so we can test this function in near isolation. Isolation is important because it limits your test’s culpability for outside interference itself. Over time all of your pieces will be validated by tests, so you can ensure the entire system is “correct.”

Take, for example, the majaxMediaFilenameBuilder. I tested it! Why? To make sure I can trust it! It should work as expected. Along the way to ensuring it worked as expected, I realized while the code worked fine, the implementation was hosed.

Now, why did I make it a separate class? It’s just a filename you say? HAH! Fat chance! For many instances, sure, it’s just a file name. But what if you wanted to make the filename harder to guess? Well, if that part of the code wasn’t replaceable, you would be out of luck. Now you just have to extend majaxMediaFilenameBuilder, override render() to return md5(parent::render(args)).’.’.$extension; and you’re golden! Don’t you just love OOP?

3. Just keep working at it

1% code coverage becomes 5%, which becomes 10%, 20%, 40%, 80%. Soon enough, you’ll find yourself checking your tests to make sure you haven’t mucked anything up, and that’s when you’ll get it. That’s when it will really hit home. It found a show-stopping bug you wouldn’t have noticed in some other part of the system that was not really related at all to what you were working on. It saved your bacon.

And that’s when it gets real.

Taking cache invalidation further…

Someone pointed out to me (quite correctly) that while the solution I offered before would work, it wasn’t testable, so I just wanted to make a quick note on how I would solve that.

The easiest solution I see, would be using the Symfony Event system.

For the sake of simplicity, I’m going to keep the listener and init code within the object. I feel it keeps maintainability better, and it allows for easy mock extension and overriding with explicit test code.

WARNING: I have not played with events yet. This is how I see it playing out, but I’m sure this steps on a few best practices. Feel free to let me know if you have a better implementation idea and I will update this page or give you a direct reference to your blog, whichever you prefer.

Here’s how I see the Model looking with this change:

<?php
 
class MyObject extends Doctrine_Record
{
  public function setUp()
  {
    parent::setUp();
    static::initEventListener();
  }
 
  protected static $listenerInitialized = false;
  public static function initEventListener()
  {
    if (ProjectConfiguration::hasActive() && self::$listenerInitialized == false)
    {
      static::doInitEventListener();
      self::$listenerInitialized = true;
    }
  }
  public static function doInitEventListener()
  {
    $dispatcher = ProjectConfiguration::getActive()->getEventDispatcher();
    $dispatcher->connect('MyObject.object_updated', array('MyObject', 'clearCacheEntries'));
  }
  // We could force-type this to sfEvent, but we don't /actually/ look at the event object...
  //so there is no need to require it.
  public static function clearCacheEntries($event = null)
  {
    cacheAssistant::clearCachePattern('**/**/pages/index');
  }
 
  private $pendingChangeNotification= false;
  public function preSave($event)
  {
    if ($this->isModified())
      $this->pendingChangeNotification= true;
  }
  public function postSave($event)
  {
    if ($this->pendingChangeNotification && ProjectConfiguration::hasActive())
    {
      $dispatcher = ProjectConfiguration::getActive()->getEventDispatcher();
      $dispatcher->notify(new sfEvent($this, 'MyObject.object_updated'));
      $this->pendingChangeNotification= false;
    }
  }
}

This way, you could even attach a mock event listener by overriding doInitEventListener to attach a different processor to the event, or overriding clearCacheEntries. It does add quite a bit of complexity to the setup, but if you’re concerned about testability, this would let you accomplish the same goals.

twitter