Symfony

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.

6 things to know about embedded forms in Symfony

When using symfony, if you use it long enough, pretty soon you will need to use embedded forms. The down side of this is, embedded forms can be problematic at best. Seeing as I have spent a lot of time getting embedded forms working just right, let me show you some of the things I have learned, so that maybe I can spare you from the same fate as our friend on the right.

1. save() is only called on the root form

That’s right! Only the root form has save() called. So if there’s other logic you want to run, you will want to override the saveEmbeddedForm method and call that code before. Oversimplification ahead: when you save a form with embedded forms, it calls $this->getObject()->save(), then it calls saveEmbeddedForms, which, for each embedded form, calls $form->saveEmbeddedForms() and then calls $form->getObject()->save(). This is critical to know, as it will save you a lot of headaches later on.

2. Extend classes for simplicity and security

When you’re working on embedding a form into a related object’s form, do yourself a favor and always extend your form to remove unneeded fields such as id, and the foreign key field. It will make your life much simpler.

For example, here’s the configure() command from my embedded version of the media registry form in my majaxDoctrineMediaPlugin:

The only reason I’m bringing the embedded form in is to be able to assign photos, videos, and audio clips to galleries, so all I really care about is $this[‘galleries_list’]. Here’s how I use it then, in the Video form:

You see there how we pass the instance that is related to our object to the embedded form? That’s why we can unset all of the foreign keys… because it already knows about the relation! Removing the foreign keys keeps it simpler, and improves security.

3. Embedded many-to-many relations are TRICKY

Ok, well, they are if you don’t know what I’m about to tell you. But then you will. Remember when we were talking about save() and saveEmbeddedForms() and how saveEmbeddedForms() doesn’t call the embedded form’s save() function? Well, this is why many-to-many relations break down in embedded forms. The function to save those relations are never called. Even better, because the embedded forms are never officially bound, they don’t even store the data to run those functions. To solve this, I use a two-prong attack.

First, I trick it into allowing us to access the values passed through bind:

Then, after being tricked into thinking it’s a real form, we close the deal by forcing it to run the function Doctrine built to save our many-to-many relationship:

4. I do it MY SELF

While watching the screens roll by in #symfony (on irc.freenode.org), I often see people having this problem or that problem with related forms. Invariably, they will be using the embedRelation command. To be honest… I don’t know what this does. I do know people seem to have lots of trouble with it though! I know, it saves time. I know, it makes it easy. I know, it’s a stock function, so you should use it, instead of expending more effort.

I also know that my forms work! I’m a huge fan of Doing What Works(tm) and Getting The Job Done(tm). It may take an extra 3 minutes to extend your form class (point #2), and hack it to make your many-to-many relations work (point 3), but it’s a whole heck of a lot better than slamming your head into the wall (or keyboard) repeatedly.

5. Post validators only fire on the root form

Another reason to simply consider your embedded form a different beast than the form on it’s own, is that any post validators you have set to run in the embedded form will not be run! However, you can access your embedded form’s data in the root form’s post validator.

Here’s an example of how to make such a validator:

6. embedMergeForm

This is something picked up from Roland Tapken. It’s really quite beautiful. Before embedMergeForm, you had two options when it came to bringing two forms together. On one hand, you have mergeForm, which looks nice, but for all intents and purposes, without a lot of extra work, doesn’t work. On the other, you have embedForm, which works for many situations, but in some circumstances (the admin generator) it will produce horrible results visually. embedMergeForm is the key! It combines the beauty of mergeForm with the mostly functional setup of embedForm.

Just to make it real easy, here’s the code I use. Place this in your lib/form/BaseFormDoctrine.class.php:

  /**
   * Embeds a form like "mergeForm" does, but will still
   * save the input data.
   */
  public function embedMergeForm($name, sfForm $form)
  {
    // This starts like sfForm::embedForm
    $name = (string) $name;
    if (true === $this->isBound() || true === $form->isBound())
    {
      throw new LogicException('A bound form cannot be merged');
    }
    $this->embeddedForms[$name] = $form;
 
    $form = clone $form;
    unset($form[self::$CSRFFieldName]);
 
    // But now, copy each widget instead of the while form into the current
    // form. Each widget ist named "formname|fieldname".
    foreach ($form->getWidgetSchema()->getFields() as $field => $widget)
    {
      $widgetName = "$name-$field";
      if (isset($this->widgetSchema[$widgetName]))
      {
        throw new LogicException("The forms cannot be merged. A field name '$widgetName' already exists.");
      }
 
      $this->widgetSchema[$widgetName] = $widget;                           // Copy widget
      $this->validatorSchema[$widgetName] = $form->validatorSchema[$field]; // Copy schema
      $this->setDefault($widgetName, $form->getDefault($field));            // Copy default value
 
      if (!$widget->getLabel())
      {
        // Re-create label if not set (otherwise it would be named 'ucfirst($widgetName)')
        $label = $form->getWidgetSchema()->getFormFormatter()->generateLabelName($field);
        $this->getWidgetSchema()->setLabel($widgetName, $label);
      }
    }
 
    // And this is like in sfForm::embedForm
    $this->resetFormFields();
  }
 
  /**
   * Override sfFormDoctrine to prepare the
   * values: FORMNAME|FIELDNAME has to be transformed
   * to FORMNAME[FIELDNAME]
   */
  public function updateObject($values = null)
  {
    if (is_null($values))
    {
      $values = $this->values;
      foreach ($this->embeddedForms AS $name => $form)
      {
        foreach ($form AS $field => $f)
        {
          if (isset($values["$name-$field"]))
          {
            // Re-rename the form field and remove
            // the original field
            $values[$name][$field] = $values["$name-$field"];
            unset($values["$name-$field"]);
          }
        }
      }
    }
 
    // Give the request to the original method
    parent::updateObject($values);
  }

Software Updates

I just wanted to make a quick post to update everyone on the progress I’ve been making on my symfony plugins.

majaxDoctrineMediaPlugin (github repository and symfony plugin)

A number of things have happened here:

  1. It’s now in the symfony plugin repository!
  2. All of it’s dependencies have been properly configured in the pear package.xml
  3. It gained concurrency support to keep from wasting CPU and mangling the files it processes.

majaxPheanstalk (github repository and symfony plugin)

It is now in the symfony plugin repository!

majaxJqueryPlugin (github repository and symfony plugin)

It is now in the symfony plugin repository!

majaxMarkdownPlugin (github repository and symfony plugin)

It is now in the symfony plugin repository!

A simple example on how to use post validators in symfony

Someone came into the #symfony channel on freenode asking how to add conditional validators that would require a text box to be filled out when a checkbox was ticked. As I realize this is a common question (though the circumstances may change), I wanted to put this example up here as well, for others.

Here’s the example:

class TestForm
{
  public function configure()
  {
    // ... snip ...
 
    $this->validatorSchema->setPostValidator(
      new sfValidatorCallback(array('callback' => array($this, 'contextualChecks')))
    );
  }
 
  public function contextualChecks($validator, $values)
  {
    if ($values['checkbox_1'] == 1)
    {
      if ($values['textbox_for_checkbox_1'] == '')
      {
        $error = new sfValidatorError($validator, 'Field is required when Checkbox 1 is checked.');
        $ves = new sfValidatorErrorSchema($validator, array('textbox_for_checkbox_1' => $error));
        throw $ves;
      }
    }
    return $values;
  }
}

Note: I completely left out returning $values in the initial draft. I apologize!

Jack and the majaxPheanstalk

I have just made my first release to GitHub, majaxPheanstalkPlugin for symfony 1.4.x (possibly will work on older ones). It’s a set of tools that help to access Beanstalkd and make building workers much simpler.

It builds off of the work of Paul Annesley (pda)‘s pheanstalk which provides a simple php library for accessing a Beanstalkd server.

The goal is to provide you with simple tools for managing your Beanstalkd integration. Right now it’s just worker thread tools and a simple factory, but I hope to grow it into a more complete library as time goes on.

twitter