test.ical.ly | getting the web by the balls

Apr/10

26

Best practice of PHPUnit testing a symfony 1.4 plugin

escher-moebiusI started this blog to occupy myself with topics like unit testing, continuous integration and code quality in general. As a subject for my studies I quickly released a symfony plugin (sfImageTransformExtraPlugin) that I already worked on for a while and decided to excercise all my learnings on it. So you can peek at the sources to see more details.

As all the early posts of this blog were kept in German I am going to summarise the best practice of PHPUnit testing a symfony 1.4 plugin in this post.

Of course I have to say that the term best practice refers to my current state of knowledge. :)

Why PHPUnit?

Actually when I started this blog I was determined to use symfonys own lime unit test implementation because it is integrated and documented within the symfony documentation. But the more I worked with it the more I was put off by it.

lime is ok for being run during development by the developer itself but it can not easily be used in continuous integration as it is not fully xUnit compliant and does not provide all infos you want to see there. lime is also very procedual and the more complex your application become the more spaghetti code you will have to maintain. And foremost lime is only used in the symfony world and learning it will not have any benefit for you in other projects.

PHPUnit on the other hand is the de facto standard for PHP. It is object oriented as well as well documented and maintained and it is fully xUnit compliant thus can easily be integrated into your continuous integration workflow.

Prerequisites / Assumptions about your plugin

In the following I assume that you generated your plugin by running the symfony task generate:plugin (provided by sfTaskExtraPlugin) and that you did not change the files and directories inside your plugins /test directory.

This is important as there are many files that were generated for you that may look curious to you but will make a lot more sense once you learned how to use them.

Of course these files were created with lime in mind and in the following I will show you what you need to change to use the same structure with PHPUnit.

Bootstrapping

In your plugins /test/bootstrap you will find a script for bootstrapping for your unit tests unit.php.

// /plugins/myTestPlugin/test/bootstrap/unit.php
getSymfonyLibDir().'/vendor/lime/lime.php';
function myTestPlugin_autoload_again($class)
{
  $autoload = sfSimpleAutoload::getInstance();
  $autoload->reload();
  return $autoload->autoload($class);
}
spl_autoload_register('myTestPlugin_autoload_again');
if (file_exists($config = dirname(__FILE__).'/../../config/myTestPluginConfiguration.class.php'))
{
  require_once $config;
  $plugin_configuration = new myTestPluginConfiguration($configuration, dirname(__FILE__).'/../..', 'myTestPlugin');
}
else
{
  $plugin_configuration = new sfPluginConfigurationGeneric($configuration, dirname(__FILE__).'/../..', 'myTestPlugin');
}

There are two things to notice here:

  1. The use of $_SERVER['SYMFONY']

When executing unit tests you have to tell the bootstrap where to look for the symfony core libraries. For lime this is actually done by the symfony test tasks but for PHPUnit we don’t have a task available so we need to run the following command first (only once per console session).

$ export SYMFONY="/absolute/path/to/lib/vendor/symfony/lib"

I will explain the reason behind this in a moment.

  1. The project configuration is looaded from a fixture project

If you have a look at your plugins /test/fixtures/project directory you will recognise a complete symfony project just like you run generate:project.

This is going to be your test environment.

The reason for both of these two features is this:

The whole point of a symfony plugin is to be reusable and to be reusable it needs to be as project independant as possible.
Therefor it will not be unit testet in a project where the project developers can easily create situations where your tests will fail but in a fixture project you as a plugin developer can completely control.

And because this fixture project is included in your project we don’t want to also include the whole symfony code.
But a symfony plugin will always have at least one dependancy and that is towards symfony itself, so we have to make the location of it known to our plugin before running the tests.

Now for PHPUnit we need to replace the following lines from the code above.

$configuration = new sfProjectConfiguration(dirname(__FILE__).'/../fixtures/project');
require_once $configuration->getSymfonyLibDir().'/vendor/lime/lime.php';

With these.

$projectPath = dirname(__FILE__).'/../fixtures/project';
/** configuration of the fixture project */
require_once($projectPath.'/config/ProjectConfiguration.class.php');
$configuration = new ProjectConfiguration($projectPath);

The purpose of this bootstrap file is to create the environment our plugin expects. So if all of your plugins functionalities need an instance of sfContext you would create it here or if they would all depend on the database you would create a connection here as well.

But remember there should be as few dependencies as possible!

How to write your test cases?

This is the easies part of all. Just refer to the PHPUnit documentation and see how to write the tests themselves and add everything to classes like this:

// /test/unit/lib/yourPluginClassTest.php

The above example will test your method yourPluginClass::yourMethod() so you see the conventions of appending "Test" to the class and prepending "test" to the methods your going to test.

I found it a good practice to organise my test cases the same way as the tested classes.

For example:
/lib/routing/yourOwnRoute.class.php (classname: yourOwnRoute)
would be tested in:
/test/unit/lib/routing/yourOwnRouteTest.php (classname: yourOwnRouteTest)

Another good practice is to have one test case per class so each of your classes has exactly one test class.

You can run your test case like this.

$ phpunit /test/unit/lib/routing/yourOwnRouteTest.php

How to write a test suite?

Often you want to run all your test cases at once. In fact for continuous integration purposes this is always the case. This can be achieved using a PHPUnit test suite that can run multiple test cases.

A good naming convention is to create such a suite in the /test directory of your plugin and to call it like you called you plugin.

// /test/yourPluginTests.php
getPluginConfiguration('yourPlugin');
    // instantiate a fake symfony unit test task to retrieve all connected tests for this plugin
    $task = new sfTestUnitTask($configuration->getEventDispatcher(), new sfFormatter());
    $event = new sfEvent($task, 'task.test.filter_test_files', array('arguments' => array('name' => array()), 'options' => array()));
    $files = $pluginConfig->filterTestFiles($event, array());
    $suite->addTestFiles($files);
    return $suite;
  }
}

In this example I collected all test cases from the fixture project just like the symfony test task for lime does it. This is very convenient as you don't have to manually add new test cases. Refer to the AllTests section of the PHPUnit documentation for further details of a test suite.

Next we need to make all our test cases known to the fixture project by editing the ProjectConfiguration class.

// taken from http://trac.symfony-project.org/browser/plugins/sfImageTransformExtraPlugin/trunk/test/fixtures/project/config/ProjectConfiguration.class.php
pluginConfigurations['sfImageTransformExtraPlugin']->connectTests();
  }
}

Now you can run all your test cases like this.

$ phpunit /test/yourPluginTests.php

The fixture project

I already mentioned the dependency towards symfony but there is more. There is also the dependency of your plugin towards being properly installed.
This dependency has to be reflected in the fixture project.

For example if you have any schema defined for Doctrine or Propel then the classes generated from these schema will be generated outside your plugin except for the classes prefixed with "Plugin". So you have to create these generated classes inside of the fixture project as well.
Make sure not to modify these classes they have to be absolutely pristine!

If your plugin needs an application youn have to create one in the fixtrue project and if certain settings in the app.yml or setting.yml or elsewhere are mandatory you will have to create them in the fixture project as well.

Just be aware that you don't try to fix anything by only adding more to the fixture project. There should be as few changes to it as possible and all changes you have made should be properly documented as they are necessary for anybody who wants to use your plugin.

Dependencies to other plugins

Sometimes your plugin will depend on the existence of other plugins. For sfImageTransformExtraPlugin this was the case as it required sfImageTransformPlugin to be installed. In your fixture project this can be solved by editing the ProjectConfiguration class.

// taken from http://trac.symfony-project.org/browser/plugins/sfImageTransformExtraPlugin/trunk/test/fixtures/project/config/ProjectConfiguration.class.php
setPlugins(array('sfImageTransformExtraPlugin', 'sfImageTransformPlugin', 'sfDoctrinePlugin'));
    $this->setPluginPath('sfImageTransformExtraPlugin', dirname(__FILE__).'/../../../..');
    $this->setPluginPath('sfImageTransformPlugin', dirname(__FILE__).'/../../../../../sfImageTransformPlugin');
  }
  // ...
}

This expects sfImageTransformPlugin to be located right next to sfImageTransformExtraPlugin which comes in handy if integrated with continuous integration services.

What to test?

For quite some time I kept asking this question. My answer is this:

  • Don't test module code! Module code in this case means actions or components. They have far too many dependencies in symfony 1.x and they should be very slim anyway. Test them with functional tests.
  • Don't test tests! This should be self explaining really.. ;)
  • Test everything else! This leaves you with the /lib and /config folders only.

And to keep a good overview you should create a coverage report from time to time. Using the following configuration file I use to create mine.



  
    
      ./test/unit/
    
  
  
    
      ./lib/
      ./config/
    
  

Change into the root directory of your plugin to execute phpunit other wise the relative paths will not work as they are relative to the execution dir not the configuration file!

Conclusion

PHPUnit testing of symfony plugins is tricky as it is not documented anywhere. But it is still very possible and once you got the hang of it feels very natural.

I hope this will help some of you t get started.

Things like mocking database connections or all the hazzle with sfContext might be subject to another post.

· · · · · · ·



  • Pingback: Tweets that mention New article: #Best #practice of #PHPUnit #testing a #symfony 1.4 #plugin #in -- Topsy.com

  • lvanderree

    thanks for a great article!
    I will apply some of this to our project soon and keep you posted with the results.

  • Pingback: • Noch mehr Erfahrungen mit PHPUnit Testing von symfony Plugins | test.ical.ly

  • Pingback: • Bootstrap Besonderheiten für Unit Tests von symfony Plugins – Teil 2 | test.ical.ly

  • Pingback: uberVU - social comments

  • http://ksojkotech.wordpress.com/ Karol Sójko

    Thank you !
    this is so useful. I’m kind of used to nUnit for .NET unit testing and with symfony I’ve used lime till now. PHPUnit seems a lot better with being OO. Thanks for this useful post !

  • Pingback: Test.ical.ly Blog: Best practice of PHPUnit testing a symfony 1.4 plugin | Development Blog With Code Updates : Developercast.com

  • Pingback: Webs Developer » Test.ical.ly Blog: Best practice of PHPUnit testing a symfony 1.4 plugin

  • maksim_ka

    You can look at the sfPhpunitPlugin. In the last svn commits was added a feature to handle plugin`s phpunit tests. All you want to do is to keep your phpunit test in %PLUGIN_NAME%/test/phpunit directory.

    The command to run all project tests and all plugin’s tests is:

    symfony phpunit:runtest –and-plugins

  • http://test.ical.ly Christian

    @maksim_ka I already had a look at those plugins. But I don’t want another dependency just for testing convenience. Also for testing plugins it is important to test them outside a project context so they keep independant.

  • Pingback: • Best practice of PHPUnit testing a symfony 1.4 plugin | test.ical.ly | Programming Blog Imagik.org

  • maksim_ka

    The only one dependency is that you have to put your test inside test/phpunit directory (may be I solve that and sfPhpunit will look for the test in test dir also). That`s it. and use suite to init all the stuff you need. In this way you can run your tests through out phpunit directly and other people can see how the tests are working on their machines and environment using the sfPhpunitPlugin.

    If you dig into the plugin more(it is pity I haven’t written a good documentation about the plugin yet) you can find a lot of helpful functions.

    “Also for testing plugins it is important to test them outside a project context so they keep independant.” – you can tests it without any context or init your own if you need.

  • http://test.ical.ly Christian

    @maksim_ka the dependency I was refering to is the one to sfPhpunitPlugin itself.
    Take a look at http://automat.ical.ly/job/sfImageTransformExtraPlugin/scmPollLog/? this is my testing environment for my symfony plugins.
    There are only three checkouts 1. symfony, 2. my plugin, 3. the plugin mine depends on.

    There is no running of tasks and no application. the bare essentials only.

  • http://test.ical.ly Christian

    @maksim_ka or actually have a look at http://test.ical.ly/2010/05/03/running-phpunit-tests-for-your-symfony-plugin-in-phpundercontrol/ I think this better explains my reasons. :)

  • Pingback: • Running PHPUnit tests for your symfony plugin in phpUnderControl | test.ical.ly

  • http://blog.inuar.com Nei Rauni Santos

    Why should I use phpUnit instead of lime?? there is some big reasons??

    I’ll check it out if you’ve ever posted something about that.

    Thanks

  • http://test.ical.ly Christian

    @Nei I did write a post on this http://bit.ly/afRo03 but it’s in German..
    The reasons in short are:
    1. The integration of lime in symfony consists only of a couple of convenience tasks that are not really necessary
    2. You will never find lime outside symfony 1.x
    3. lime does not support setup() or teardown() methods
    4. lime is not xUnit compliant which limits a continuous integration use to the bare essentials
    5. lime itself is not documented!
    6. lime tests are procedural and can become very messy very quickly
    7. lime is not continued in Symfony 2 (Lime 2 is a standalone project) which will use PHPUnit
    8. PHPUnit is the de facto standard for PHP
    9. PHPUnit is well documented
    10. PHPUnit use in continuous integration servers is well documented in many many blogs

    Well there are more but I guess you get the point.

    In the end what matters is that the tool you use suits you. I thought that lime would suit me but after some serious trying out it didn’t.

  • http://www.kennethvr.be Kennethvr
  • Roman

    “Don’t test module code! Module code in this case means actions or components.” – Don’t I need to Unit Test my actions?

  • http://test.ical.ly Christian

    @Roman you do need to test your actions but not with unit tests. instead you should write functional tests for them. you can either use lime or selenium or Behat for it.

  • Roman

    Thanks for the answer!
    But why not to have Unit Tests?
    There’s one redirect to external resource in my scenario which makes functional test sequence split. Also my actions class has a lot of private methods to send / receive data. This data is critical, so I think it would be good idea to have Unit Tests for the internals of my app.

  • http://test.ical.ly Christian

    @Roman there are many things to answer this. first of all your action should be as slim as possible and I guess that your private methods don’t really belong in there but instead should go into model, query, form or whatever classes. the controller should only be a chain of command. and because there are external resources the term unit test wouldn’t fit. for a complete user scenario you always need an acceptance test a.k.a. functional test. a unit test should only test the smalles parts of your code: the methods. testing private methods again is a whole different story.

  • http://test.ical.ly Christian

    @Roman try some of the posts for the “unit tests” and “tests” tags on my blog I’m sure I wrote a couple on this topic. :)

  • Roman

    Great! Thanks for the hint, Christian!

<<

>>

Theme Design by devolux.nh2.me