20 Aug 2009, 10:31 p.m.

Lightweight Continuous Integration for PHP

This post describes a simple, lightweight strategy for implementing Continuous Integration on PHP-based software projects. This approach happens to use Subversion for version control, PHPUnit for unit testing, and Phing to automate the processes involved, but hopefully the principles are generally applicable.

The post won't strive to be exhaustive or encyclopaedic, rather it will present a simple proof-of-concept and a brief overview of the tools that are available to PHP developers. But first some background...

Why Continuous Integration?

Continous Integration is...

...a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily - leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible.

The benefits of this practice include knowing that you always have a fully-working build of your application should you need to deploy at short notice. Moreover, you'll catch bugs and broken tests as soon as they happen, rather than some time later, when it'll be much harder to retrace your steps and debug the problem, or when a release deadline is looming.

A lot of literature exists covering CI, so I won't dwell here. Instead, I'll point readers in the direction of Martin Fowler's excellent Continuous Integration article, and also the PHP-specific coverage in the July 2009 issue of php|architect magazine.

Why Lightweight?

PHP developers currently have a couple of options for implementing CI. There's phpUnderControl, and also Xinc. phpUnderControl in particular is a wonderful and very powerful piece of software, but neither of these solutions is trivial to implement. Installation can be complex, and both require quite a large shift in mindset for the development team.

Sometimes what's needed is a simple, unobtrusive method of continuously monitoring the code that is committed to version control, and making sure that unit tests continue to run. Fortunately, it's quite possible to manage this using a handful of standard, open source tools.

Unit Tests

The first point to note is that, PHP being an interpreted rather than a compiled language, there's very little value in implementing CI unless your application has unit tests. That said, delivering unit tests along with code is part and parcel of professional software development, so that really shouldn't be a problem.

Let's start with some trivial example code. Try not to get distracted by the abject pointlessness of this class:


<?php

class Calculator
{
    public function add($a, $b)
    {
        return $a + $b;
    }

    public function multiply($a, $b)
    {
        return $a * $b;
    }
}

Now to add some simple tests for that. In this case we're using PHPUnit (documentation, installation), though the principle should be much the same with SimpleTest:


<?php

class CalculatorTest extends PHPUnit_Framework_TestCase
{
    public function testAdding()
    {
        $calc = new Calculator();
        $this->assertEquals($calc->add(1, 4), 5);
        $this->assertEquals($calc->add(17, 18), 35);
    }

    public function testMultiplication()
    {
        $calc = new Calculator();
        $this->assertEquals($calc->multiply(1, 4), 4);
        $this->assertEquals($calc->multiply(17, 18), 306);
    }
}

We can run the tests from the command line, and all is good:

[simon@vps02 tests]$ phpunit tests.CalculatorTest.php
PHPUnit 3.3.4 by Sebastian Bergmann.

..

Time: 0 seconds

OK (2 tests, 4 assertions)

So now that we have a codebase with full test coverage, we're in a very good place to start implementing Continuous Integration. We'll use Phing to automate the process.

Bring in the Phing

Phing (documentation, installation) is a project build system for PHP applications. It's analogous to GNU make for C, or Apache Ant for Java. It's generally a very useful tool for automating processes which involve a series of dependent steps.

We'll make the assumption that the project is already imported into version control, in this case Subversion. (If not, there are some notes on how to achieve that in part 2 of my command line Subversion tutorial). That being the case, the steps we want to include in the build process are as follows:

  1. Check the code out from version control
  2. Run the tests
  3. Inform the development team if something is broken, otherwise keep quiet

We'll tackle the steps one by one.

Subversion Checkout

Checking the code out from Subversion is made particularly simple by Phing, as it has built-in support for exactly this task. First, we'll need to provide Phing with some login details for the repository, and I find it particularly convenient to store these in a Phing properties file, let's call it subversion.properties:

# subversion details
svn.path="/usr/bin/svn"
svn.username="simon"
svn.password="mypassword"
svn.base="svn://svn.example.org/"

Our Phing project file will need to import these properties, and that can be done using the <property /> task. We'll also want to create an empty directory into which we can check out the code, and perform the actual checkout using the Phing <svncheckout /> task. The initial project file looks like so:


<?xml version="1.0"?>
<project name="Test CI Build" default="runci" basedir=".">

    <!-- load the Subversion details we defined earlier -->
    <property file="subversion.properties" />

    <property name="builddir" value="./build/" override="true" />

    <target name="deletebuilddir">
        <delete dir="${builddir}" includeemptydirs="true"
                verbose="false" failonerror="true" />
    </target>

    <target name="createbuilddir" depends="deletebuilddir">
        <mkdir dir="${builddir}" />
    </target>

    <target name="checkout" depends="createbuilddir">
        <svncheckout
                svnpath="${svn.path}"
                username="${svn.username}"
                password="${svn.password}"
                force="true"
                nocache="true"
                repositoryurl="${svn.base}/test/myciproject"
                todir="${builddir}" />
    </target>

    <!-- ...more tasks to follow... -->

    <target name="runci" depends="checkout" />

</project>

The checkout task depends upon the <createbuilddir /> task, which in turn depends upon a further task which just blows away the build directory in case anything is left behind from a previous run. We'll save the the project file as, say, cibuild.xml, and we can test the checkout by running Phing from the command line:

[simon@vps02 citest]$ ls
subversion.properties  cibuild.xml
[simon@vps02 citest]$ phing -f cibuild.xml
Buildfile: /home/simon/citest/cibuild.xml
 [property] Loading /home/simon/citest/subversion.properties

Test CI Build > deletebuilddir:


Test CI Build > createbuilddir:

    [mkdir] Created dir: /home/simon/citest/build

Test CI Build > checkout:

[svncheckout] Checking out SVN repository to './build/'

Test CI Build > runci:


BUILD FINISHED

Total time: 0.8001 second

[simon@vps02 citest]$ ls
build  subversion.properties  cibuild.xml
[simon@vps02 citest]$ ls build/
Calculator.php  tests

You can see from the output from Phing that the project has been checked out to the build/ directory. Next we'll add the test run to the Phing project.

Running the Tests

Running the test suite is a further task that's made very easy indeed by Phing, thanks to the built-in <?phpunit /> task. There's also a (sadly undocumented) <simpletest /> task. Here's the XML that we need to add to the project file:


<target name="runtests" depends="checkout">
    <?phpunit failureproperty="testsfailed">
        <formatter type="plain" usefile="false"/>
        <batchtest>
            <fileset dir="${builddir}/tests">
                <include name="**/tests.*.php" />
            </fileset>
        </batchtest>
    </phpunit>
</target>

<!-- modify the runci task to depend on runtests -->
<target name="runci" depends="checkout" />

The XML specifies that any files named according to the pattern tests.*.php and found under the build/tests/ directory should be treated as test files and added to the test run. The output from Phing now shows the results of the PHPUnit test run:

[simon@vps02 lightweightci]$ phing -f cibuild.xml
Buildfile: /home/simon/lightweightci/cibuild.xml
 [property] Loading /home/simon/lightweightci/subversion.properties

Test CI Build > deletebuilddir:


Test CI Build > createbuilddir:

    [mkdir] Created dir: /home/simon/lightweightci/build

Test CI Build > checkout:

[svncheckout] Checking out SVN repository to './build/'

Test CI Build > runtests:

  [phpunit] Testsuite: CalculatorTest
  [phpunit] Tests run: 2, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.00136 s

Test CI Build > runci:


BUILD FINISHED

Total time: 1.8001 second

It's worth noting the @failureproperty attribute on the phpunit task. This is a Phing property that will be set to true should there be any failing tests. This is going to be very useful when it comes to reporting on the build.

Reporting

It stands to reason that if the test run is broken, we want to know about it. A simple approach is to email the development team, along with some basic information on the last few commits, so that we know who to blame for the failing tests. That's easy to achieve using a simple command line incantation, and Phing's built-in <exec /> task:


<target name="runci" depends="runtests">
    <if>
        <equals arg1="${testsfailed}" arg2="1" />
        <then>
            <exec command="svn log --limit 5 build/ | mail -s 'Build broken' php-dev@example.org" />
        </then>
    </if>
    <phingcall target="deletebuilddir" />
</target>


If the testsfailed property is set to true, the <exec /> task runs, triggering an email containing the five most recent Subversion log entries for the project. That's a little crude, but it should be enough to alert the team to the fact that something's wrong, and that action is needed. Note that I've added a further call to the deletebuilddir target, to clean up any files that were checked out and left lying around by the build process.

Making it Continuous

This is great progress so far: in a single step, we can now check a project out of version control and run the test suite, notifying the development team if there are broken tests. All that's left is to make sure that the integration is, well, continuous. I can see a couple of approaches to that. One option is to make the Phing build happen every, say, fifteen minutes during working hours by configuring a cron task. A crontab line for that might look something like so:

15,30,45,59 8-18 * * mon-fri phing -f cibuild.xml > /dev/null 2>&1

Another option is to trigger the process automatically whenever code is committed to the version control repository using an SVN hook. That's quite a common approach, but I'm not sure I like it. Quite often as part of committing a larger change, I'll make several small, atomic commits in quick succession, and without some extra magic in place, this could cause several instances of the integration process to run at the same time, leading to all sorts of race conditions and unexpected behaviour.

Still, the option is there, and you can make your own mind up how to go about making the build process run regularly enough to be useful.

Taking it Further

So we've seen how to implement a basic, lightweight Continuous Integration process for PHP applications using simple-yet-powerful open source tools. We could take things further by having Phing generate phpDocumentor documentation for our code, or generate PHP_CodeSniffer reports at the same time. Phing is as flexible as it is powerful, so there's no real limit to what can be done as part of a project build.

Posted by Simon in PHP, Testing and Programming
21 Aug 2009, 12:31 a.m.

Ciaran McNulty

Very interesting stuff!

So much of the CI literature seems to end up being 'how to set up CruiseControl' that it's refreshing to see how it's possible to 'roll your own'.

A third option aside from cron or an svn hook would be to run the CI as a daemon - I suspect this is what apps like CruiseControl effectively do.

21 Aug 2009, 11:42 a.m.

Russell

We use CruiseControl and it runs everytime something is commited into svn. We have no issues with instances running concurrently as CruiseControl queues them up. CruiseControl can also be set up to give a grace period of, say, 30 secs so that it doesn't run on 'atomic' commits.

I would prefer this to your alternative of running the process periodically. I could see this leading to problems particularly in a larger team or a very active project. I suppose however that you could adjust the frequency of the cron based on the size / activity of the team.

Your assertion that you "don't believe that anyone can call themselves a professional developer unless they're delivering unit tests" is possibly a little bold. We have several very good developers not writing any tests based on a decision we made to not enforce test driven development on some of our legacy code. Rightly or wrongly you may argue!

Either way this is a particularly good post even by your own high standards and an excellent guide to getting CI up and running.

21 Aug 2009, 12:10 p.m.

Simon Harris

Thanks, gents.

I should stress that I'm not questioning the validity of CruiseControl (or phpUnderControl, they're essentially the same product) and their daemons and associated magic, I'm just highlighting an alternative route for those who don't have the time/inclination/management buy-in to make a good fist of xControl at the present time, which I suspect is quite a lot of people.

Regarding unit tests, it's something I'm not ashamed to have very strong feelings about. That said, I may reword have reworded that section so as not to distract from the matter at hand.

For what it's worth, I think my gripe is with a wider issue, in that professional standards in software development are...well, there aren't any. So developers are quite used to "getting away" with very disappointing practices, and nobody really notices. In a lot of shops, if you churn out epic amounts of code, you're a hero developer, right? Perhaps that's a rant best saved for a future post, all the same.

29 Aug 2009, 8:39 p.m.

Andrew Tulloch

Been using Hudson (http://hudson.dev.java.net/) at work for several months now, primarily with maven based java projects, but it's extensible with plugins (including one for phing). Very easy to setup and once running I all web interface configurable. Can either poll svn repos or have commit hooks on the repos to inform hudson of changes. I'd recommend giving it a go.

11 Dec 2011, 9:02 p.m.

Luka

Incredibly concise article...
I am new to CI (even though I am not new to PHP OOP, design patterns and best principles) It was really bumpy voyage understanding the CI concept and need for it when I first entered into one project based on JEE/Struts, Maven and SVN...
I got it now together with the point of writing unit tests and running them on regular bases with CI tools and I'll be more than happy to try to integrate it into my future PHP development...
Thanks