You all should be testing your plugins. You know that, right?

Regardless of whether or not you know that, chances are you have NO EARTHLY IDEA how to accomplish it. I’ve spent a while getting tests up and running for the release of my plugin sfThemeGeneratorPlugin. The truth is, the native testing framework is filled with hacks. This post illustrates how to compound upon these hacks so they do your bidding. We will use the Lime testing framework bundled with symfony.

Methodology

The Why:
First of all, why test your plugins? Plugin tests ensure you won’t break your buddy’s project when you push minor fixes. Also, it gives your buddy this same confidence when upgrading to a new release. Plugins are self contained libraries. They should be able to be tested independently of a particular symfony project. In fact, they should be able to run with any symfony library you provide to them, granted the release is supported by your plugin. A plugin’s test should be independent of every dependency it has. Symfony is a dependent library, as are any plugins required for your plugin to run.

With the ability to define the paths to your plugin’s dependencies, you can quickly test your plugin across multiple releases of those dependencies. It also allows your plugin’s users to run your tests within the context of their application. Who has two thumbs and an awesome plugin? You do my friend. You do.

The How:
Our plugin should contain an executable file to run its tests. The executable should accept parameters for the paths to any of its dependencies. The plugin in my example accepts the symfony_dir argument like this:

$ cd sfMyAwesomePlugin/test/bin/
$ php prove.php
Symfony directory not found.  Please set $_SERVER['SYMFONY'] or provide a --symfony_dir argument
$ php prove.php --symfony_dir=/usr/local/lib/symfony/RELEASE_1_4_0/lib
plugins/sfMyAwesomePlugin/test/functional/sfMyAwesomePluginFunctionalTest..ok
plugins/sfMyAwesomePlugin/test/unit/sfMyAwesomePluginUnitTest........ok
All tests successful. Files=2, Tests=12

At the same time, we want to be able to run our tests as a part of our application test suite. Luckily, this part is feasible through the existing Symfony test framework.

# Task to run all tests in your plugin
$ php symfony test:plugin sfMyAwesomePlugin

The Process

So how do we do it? First, we need to include a symfony project fixture in the plugin. This is required for both unit and functional tests. We’ll add this in test/fixtures/project. Now before you post a picture of Xsibit, let me explain why this isn’t a terrible idea. Even though the symfony framework is rather robust, you can pear it down quite a bit. Take it down to bare bones and you are left with only 6 of files and 10 directories. This is the minimum directory structure needed to run a symfony project fixture:

# symfony project fixture
/path/to/plugin/fixtures/project
    /apps
         /frontend
             /config
                 /frontendConfiguration.class.php
                 /routing.yml
             /lib
                 /myUser.class.php
    /cache
    /config
        /doctrine*
            /schema.yml*
        /databases.yml*
        /ProjectConfiguration.class.php
    /data
        /fresh_test_db.sqlite*
    /lib
    /log
    /web
        /index.php

The files and folders with asterisks(*) next to them are only needed if you’re using a the database layer. For this, I strongly suggest using sqlite. We will copy the fresh database over each time the tests run. Build your test database by running the DB build task in the fixture project (For doctrine and symfony 1.3+ you would use doctrine:build --all).

You can download the symfony project fixture here. Now that we have a project fixture, we need to add our execution binary. We’ll use a prove.php file. This is a simple PHP execution file, and does nothing but include your bootstrap file and run your tests. This is also where we want to check to make sure the path to your symfony library is specified. Add this file to /path/to/plugin/test/bin/prove.php

Next we want to have a Bootstrap class. I took the liberty to make the sfPluginTestBootstrap class. I refuse to write a single line of PHP procedural code that is not strictly necessary. This file will determine the symfony directory, autoload symfony core, and register the setup and teardown functions. This class also begs you to extend it for your project if needed. Add this file to /path/to/plugin/test/bootstrap/sfPluginTestBootstrap.class.php

Finally, we need to have a bootstrap file. This is similar to the bootstrap you’ll find in any of your project’s tests. While I am no more a fan of this than you, this is the way things work with Lime. Mine looks like this: bootstrap.php. This will be the sole include at the top of each test. This will allow you to run each test manually via command line ($php sfMyAwesomePluginTest.php) and via the prove.php file. Wootness. Add this file to /path/to/plugin/test/bootstrap/bootstrap.php

The Result

When you get to this point, you should have something like this in your plugin’s test directory: Sample Plugin Test Directory

So what do we need now? The good news is… that’s pretty much it! Now we just need to add the test files themselves. These will look exactly like a regular symfony test. That’s right, you can write a unit or functional test in whatever project you’re using, and just move the files into your plugin. You may need to adjust the bootstrap include at the top of each test, but this approach follows the symfony convention, so even that is unlikely. That’s nice, right?

Now sit back and enjoy all the benefits that come with having beautiful test coverage. Confidently notate compatible symfony versions. Give your users the piece of mind knowing your plugin is compatible with their application. And refactor with confidence! I basically had to cage fight a rabid bear to figure all this out. Now all you need to do is follow these steps. Give my wounds purpose, and test your plugins.

How many of you encounter the need for complex where statements in Doctrine? What application DOESN’T require a complex where statement on occasion? Well, what I’m getting at is most of them do!.

If and when you encounter such a situation, you can choose one of three paths (but choose wisely):

  1. - Break your query into smaller, more manageable queries and execute them separately
  2. - Drop down to DQL, or even straight SQL (essentially bypassing the ORM layer) for more granular control and to utilize options not available in the Doctrine_Query object (subqueries, etc).
  3. - EXTEND THE DOCTRINE QUERY OBJECT!

One and Two are great options, and should always be considered. However, there are occasions where a query object is just more convenient. For example, if you are using the default symfony admin generator, you should be aware its core functionality requires a Query object. And hey, what’s the use of writing all this object oriented code if we can’t subclass every now and then? Plus, it’s a lot of fun.

Predicament

I want to filter a query (any query) by a date range. I want to take whatever timestamp fields are tied to this object, and make sure at least ONE of them falls in that date range. I call the filter “Occurs In Range”. Here’s an example:

  • - I have a list of assigned tasks
  • - Each Task object has an assignment date, a due date, and an action date.
  • - I want to filter my tasks by a date range, and find any tasks where any of those dates fall between my range

Task 1:

  • title: Launch that website for that friend
  • description: Jack wants a website professing his love for his cat. He wants you to make it for him.
  • start date: April 1st, 2010
  • end date: April 30th, 2010
  • action date: null

Task 2:

  • title: Purchase Food for St. Patrick’s Day Party
  • description: You are having a wild party for St. Patrick’s Day. Don’t forget cabbage!
  • start date: March 1st, 2010
  • end date: March 16th, 2010
  • action date: March 17th

How are we going to write a filter that adds this query? Sure, we could iterate over the timestamp fields, adding ->orWhere‘s. But if we have any where clause already attached to this query, it’s going to screw with the logic. Why would you let Doctrine screw with the logic? I tell you the truth brother, this is a thing of evil.

Solution

By subclassing the Doctrine_Query object, we can create our own query. I called mine Doctrine_Query_Extra, although other good suggestions were Doctrine_Pimp_Query, Doctrine_Bling_Query, and Bling_Pimp_Doctrine. I then added a few methods to allow us better handling of our where clauses. These functions include andClause for beginning a parenthetical where clause with AND, orClause for beginning a parenthetical clause with OR, and endClause for ending either clause. Here is an example of these functions in action.


  public function addOccursInRangeColumnQuery(Doctrine_Query $query, $field, $value)
  {
    $query->andClause();

    foreach ($this->getOption('occurs_in_range.filter_fields') as $i => $f)
    {
      $this->orClause()
          ->addDateQuery($query, $f, $value)
          ->endClause();
    }

    $query->endClause();

    return $query;
  }
 

How cool is that? We in effect just wrapped our entire filter in an AND statement, and made sure our individual date query statements were OR’d (i.e. only ONE of the object’s dates need to be in the range). Shazam.

Some of you are confused with the above function. “What is addOccursInRangeColumnQuery”? Quite simply, if you set an ‘occurs_in_range’ filter field to the “sfWidgetFormFilterDate” widget in your Form Filter class (something like *Model*FormFilter.class.php) and also a “occurs_in_range.filter_fields” option to your array of date fields:

  $this->setOption('occurs_in_range.filter_fields', array('start_date', 'end_date', 'active_date'));

…Doctrine and symfony will automatically look to call that method when that filter is used. Put the above code in your BaseFormFilterDoctrine class and you’ll be ship shape. Take a peak at my Code Snippets link at the bottom to see this in context.

In Doctrine 1.2+ you can tell Doctrine to use your subclass by calling

$manager->setAttribute(Doctrine_Core::ATTR_QUERY_CLASS, 'Doctrine_Query_Extra');

in your project configuration. If you are using less than Doctrine 1.2, you can create the Doctrine_Query_Extra class by calling Doctrine_Query_Extra::create(), or by overloading the Doctrine::getTable('TableName')->createQuery() method on a table by table basis, which will look something like this:

    public function createQuery($alias = '')
    {
        if ( ! empty($alias)) {
            $alias = ' ' . trim($alias);
        }
        return Doctrine_Query_Extra::create($this->_conn)->from($this->getComponentName() . $alias);
    }

You can download this class using the link below. If you use it, let me know if it works well for you, and please post here if you can think of ways to improve it!

Links

**Update**
Fixed an issue in Doctrine_Query_Extra where an error was thrown for empty clauses (i.e. $query->andClause()->endClause()). This is now fixed.