plugins

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.

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!

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.

How to build a better date widget in symfony with jQuery

We’re going to step things up a notch with this one, so hang on for the ride, because for those of you not already well versed in symfony and jQuery, there will be a lot of new information, and it will go pretty quickly, but I will try to make sure to keep everyone caught up. This is another post I hinted towards back when I first wrote about symfony widgets. Right, so, on to the fun!

First things first, let’s talk about requirements. My only real requirement is that it must be seamless. We must be able to only implement the widget, and have everything else work exactly as intended. For a date widget, this means when the user clicks that submit button, an array with ‘month’, ‘day’, and ‘year’ must be sent back. Not a string, not JSON, just that basic array.

It’s for the above reason that we will need to make a widget on top of the basic jQuery UI Datepicker Widget. The Datepicker alone would just return a string when we submitted the form, which would then require at least a custom validator, if not much more. So to simplify things later, we’ll put in more effort now.

There are four things we will be doing in this tutorial to make this happen. Here’s a quick overview:

  1. Set up majaxJqueryPlugin to give us our jQuery/jQuery UI base
  2. Set up a new module for us to use with the widget tutorials
  3. Create our widget class
  4. Create our jQuery UI widget that goes along with our widget class

1. Setup majaxJqueryPlugin

We’re using my plugin for two reasons:

  1. It’s my blog, so of course, I’m using my plugin
  2. We need jQuery UI, both the JavaScript and the CSS, and majaxJqueryPlugin provides that right away

Setting it up is easy:

cd plugins
wget -O majaxJqueryPlugin.zip http://jmather.com/wp-content/plugins/download-monitor/download.php?id=4

Now we add the plugin to our Project Configuration. Open up your config/ProjectConfiguration.class.php file, and place this line somewhere within your setup() function:

    $this->enablePlugins('majaxJqueryPlugin');

And one last step, we need to publish the assets:

./symfony plugin:publish-assets

Alright, now we’re ready to move on to the good stuff.

2. Setting up our plugin

Now we need to make a new plugin, and set up a few directories for future use. It’s pretty self-explanatory so we’ll cut right to the code, and come back afterwards to talk about some of the extras.

./symfony generate:plugin majaxWidgetPlugin
mkdir -p plugins/majaxWidgetPlugin/web/js
mkdir -p plugins/majaxWidgetPlugin/lib/widget

We made our plugin, then we made the directory our jQuery plugins will go in, and the directory where we will store our widgets. The -p flag means “no error if existing, make parent directories as needed” which saved us a few mkdir commands.

3. Making our symfony widget

Now open up plugins/majaxWidgetPlugin/lib/widget/majaxWidgetFormDate.class.php and here’s our code:

<?php
 
class majaxWidgetFormDate extends sfWidgetFormDate
{
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    $sfContext = sfContext::getInstance();
    $resp = $sfContext->getResponse();
    $resp->addJavascript('/majaxWidgetsPlugin/js/jquery.majax.dateselector.js');
 
    $id = $this->generateId($name);
 
    $txt = ($this->getOption('can_be_empty') == true) ? 'true' : 'false';
 
    $display = '<div id="'.$id.'"></div>';
    $display .= '<div id="'.$id.'_ctrls">';
    $display .= parent::render($name, $value, $attributes, $errors);
    $display .= '</div>';
    $display .= '
<script type="text/javascript">
$(function() {
  $(\'#'.$id.'\').majaxdateselector({can_be_empty: '.$txt.'});
});
</script>
';
    return $display;
  }
}

So, to start out, we extend sfWidgetFormDate, as that will make the controls we need to manage a date submission. The only change we need to do, is to override the render function, which will let us add some custom code around the standard output, to allow us to more easily control the display.

The first thing we do in the render function is to include the required JavaScript. Some will say the widget shouldn’t know about the JavaScript, but I say, if the widget shouldn’t know… who should? We want to make this as transparent as possible, so why should we have to also remember to add a JavaScript include to use this widget?

Next, we grab our generated ID, and figure out our options (i.e. can the date be null) that we can pass along to Datepicker. After that we wrap the whole deal in a containing div, and then wrap the native controls in another specific div. You will see why we did this in a minute. Lastly, we write the JavaScript to activate the widget.

4. Making our jQuery Plugin

As is the standard, I’ll show you the code, then highlight portions that are interesting. This file goes in plugins/majaxWidgetPlugin/web/js/jquery.majax.dateselector.js

(function($) {
        $.widget('ui.majaxdateselector', {
                version: '1.0.0',
                eventPrefix: 'majax.dateselector',
                options: {
                        can_be_empty: false,
                        datepicker_opts: {
 
                        }
                },
                _create: function() {
                        this.options['id'] = $(this.element).attr('id');
                        this._hide_real_ctrls();
                        this._build_facade();
                        return this;
                },
                _build_facade: function() {
                        $(this.element).html('<input size="10" type="text" id="'+this.options['id']+'_display" />');
                        var tfDisplayUpdate = function(widget) {
                                return function() {
                                        widget._update_ctrls(this.value);
                                }
                        }
 
                        $('#'+this.options['id']+'_display').change(tfDisplayUpdate(this));
 
                        var m, d, y;
                        m = $('#'+this.options['id']+'_month').val();
                        d = $('#'+this.options['id']+'_day').val();
                        y = $('#'+this.options['id']+'_year').val();
                        if (parseInt(m) > 0 && parseInt(d) > 0 && parseInt(y) > 0)
                        {
                                $('#'+this.options['id']+'_display').val(this._zero_pad(m, 2)+'/'+this._zero_pad(d, 2)+'/'+y);
                        }
                        $('#'+this.options['id']+'_display').datepicker(this.options['datepicker_opts']);
                        if (this.options['can_be_empty'])
                        {
                                $('#'+this.options['id']).append(' <input type="button" id="'+this.options['id']+'_empty" value="Clear" />');
                                $('#'+this.options['id']+'_empty').button();
                                var tfClear = function(widget) {
                                        return function() {
                                                widget._clear_display();
                                                return false;
                                        }
                                }
                                $('#'+this.options['id']+'_empty').click(tfClear(this));
                        }
                },
                _zero_pad: function(num,count)
                {
                        var numZeropad = num + '';
                        while(numZeropad.length < count) {
                                numZeropad = "0" + numZeropad;
                        }
                        return numZeropad;
                },
                _clear_display: function() {
                        $('#'+this.options['id']+'_display').val('');
                        $('#'+this.options['id']+'_month').val('');
                        $('#'+this.options['id']+'_day').val('');
                        $('#'+this.options['id']+'_year').val('');
                },
                _update_ctrls: function(val) {
                        var vals = val.split('/');
                        if ((val == '' || vals.length != 3) && this.options['can_be_empty'])
                        {
                                $('#'+this.options['id']+'_month').val('');
                                $('#'+this.options['id']+'_day').val('');
                                $('#'+this.options['id']+'_year').val('');
                        }
 
                        var m, d, y;
                        m = vals[0];
                        d = vals[1];
                        y = vals[2];
 
                        if (parseInt(m) > 0 && parseInt(d) > 0 && parseInt(y) > 0)
                        {
                                $('#'+this.options['id']+'_month').val(parseInt(m));
                                $('#'+this.options['id']+'_day').val(parseInt(d));
                                $('#'+this.options['id']+'_year').val(parseInt(y));
                        }
                },
                _hide_real_ctrls: function() {
                        $('#'+this.options['id']+'_ctrls').css('display', 'none');
                },
                _show_real_ctrls: function() {
                        $('#'+this.options['id']+'_ctrls').css('display', null);
                },
                destroy: function() {
                        this._show_real_ctrls();
                        ('#'+this.options['id']).html('');
                        $.Widget.prototype.destroy.call(this);
                        return this;
                }
        });
})(jQuery);

You know, looking over, it’s pretty clear, I feel, what most parts do. The functions “_hide_real_ctrls”, and “_show_real_ctrls” hide and show that wrapping div we built around the original controls, so we can keep them in play, but not have to worry about controlling them. The “_build_facade” function builds our fake interactive Datepicker object, and optionally our ‘Clear’ button, if our date is allowed to be empty. Functions “_clear_display” and “_update_ctrls” to exactly as you would expect. Cure functions “_create” and “destroy” are from the jQuery UI Widget framework, and are called … can you guess when? 🙂

We’re done!

Once you’re done, you can replace any sfWidgetFormDate instance with a majaxWidgetFormDate instance, and everything else is handled. A little bit of effort up front, and many rewards down the road!

How to add jQuery to your Symfony project, part 2.

In part 1 we explored why exactly we may want to build our own jQuery plugin, so now in part two, we will cover exactly how to do so.

Note: these examples are through a bash command line, as I am fairly certain that is the most common way one interacts with Symfony. If you’re using Windows, you’ll get the gist of what to do, however.

Let’s start with initializing the plugin:

./symfony generate:plugin majaxJqueryPlugin

Now we will switch to our plugin directory:

cd plugins/majaxJqueryPlugin

We’re going to be providing web resources, so we will want to make web, js, and css directories. We will also make a temp directory to download files to:

mkdir web
mkdir web/js
mkdir web/css
mkdir temp

Prepare a place to download what we need:

cd temp

Here is our download and copy code. I have decided to host it from here, as jQuery UI doesn’t seem to provide direct download links.

wget "http://jmather.com/wp-content/plugins/download-monitor/download.php?id=3"
unzip jquery-ui-1.8.2.custom.zip
cp js/jquery-* ../web/js/
cp -a css/smoothness/ ../web/css/

Let’s clean up after ourselves…

cd ..
rm -rf temp

Now for the fun part, to make it work. Since I like things to ‘just work’, here’s how I will do it. I will take a page from jQuery Reloaded and use a helper to actually load the files into the response, but instead of making people manually add it where they want it, we will just automatically shove it in if the plugin is enabled!

It’s time to make our helper directory:

mkdir lib/helper

Here is the contents of lib/helper/MajaxjQueryHelper.php:

1
2
3
4
5
6
7
<?php
$jq = '/majaxJqueryPlugin/js/jquery-1.4.2.min.js';
$jqui = '/majaxJqueryPlugin/js/jquery-ui-1.8.2.custom.min.js';
$jquicss = '/majaxJqueryPlugin/css/smoothness/jquery-ui-1.8.2.custom.css';
sfContext::getInstance()->getResponse()->addJavascript($jq, 'first');
sfContext::getInstance()->getResponse()->addJavascript($jqui, 'first');
sfContext::getInstance()->getResponse()->addStylesheet($jquicss, 'first');

For the last little trick before we enable the plugin, open up config/majaxJqueryPluginConfiguration.class.php and add the following code to the initialize() function:

   	$helpers = sfConfig::get('sf_standard_helpers', array());
        $helpers[] = 'MajaxjQuery';
        sfConfig::set('sf_standard_helpers', $helpers);

Your completed majaxJqueryPluginConfiguration.class.php file should look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
/**
 * majaxJqueryPlugin configuration.
 *
 * @package     majaxJqueryPlugin
 * @subpackage  config
 * @author	Jacob Mather
 * @version     SVN: $Id: PluginConfiguration.class.php 17207 2009-04-10 15:36:26Z Kris.Wallsmith $
 */
class majaxJqueryPluginConfiguration extends sfPluginConfiguration
{
  const VERSION = '1.0.0-DEV';
  /**
   * @see sfPluginConfiguration
   */
  public function initialize()
  {
   	$helpers = sfConfig::get('sf_standard_helpers', array());
        $helpers[] = 'MajaxjQuery';
        sfConfig::set('sf_standard_helpers', $helpers);
  }
}

You’re done! Now you just have to go back to your project root, and edit your project configuration to add the plugin, and publish it’s assets.

Here is the line to add to your ProjectConfiguration’s setup() function, just in case you need it:

    $this->enablePlugins('majaxJqueryPlugin');

To publish it’s assets:

./symfony plugin:publish

And now you’re ready to use jQuery throughout your Symfony project!

To download a copy of the majaxJqueryPlugin I have made (and save yourself some work!), use the link below, and you just have to follow the last step to enable the plugin in your project:

Download majaxJqueryPlugin v1.0.0 (109.06 kB zip)
twitter