Although I am primarily a ruby developer, I maintain several legacy PHP applications. By legacy, I mean Michael Feather's definition of legacy - code without tests. Sometimes it's an absolute beating, but if you know me, you know that I'm stubbornly persistent about finding ways to test legacy applications. Writing smoke tests before fixing a bug or adding a new feature gives me a reasonable degree of confidence that I am not introducing new bugs.

As a ruby developer, I am spoiled by great open source tooling. Among my favorites are Rake, for running tasks, and Guard for running contextual tasks in response to file modifications. In this post I will show you how to use both Rake and Guard with PHPUnit to make running your PHP tests as pleasurable of an experience as running your ruby tests.

TL;DR

Copy the files in the following gist to your PHP project and modify them to match the directory structure of your application.

https://gist.github.com/jbgo/c52200c29559bf2b0bc6

Assumptions

To keep this post as concise as possible, I make the following assumptions.

  • You have an existing, working PHPUnit test suite for your application.
  • You also have a working ruby environment on your computer.

Step 1. Create the Gemfile

First let's install the gems we need to make this work. Creating a file named Gemfile in your project's root directory, then run bundle install.

source "https://rubygems.org"
gem "guard-rake"

The guard-rake gem is a guard plugin that runs rake tasks in response to file modifications. We'll learn more about what that means exactly when we create our Guardfile in the next step.

Step 2. Create the Guardfile

Place the following Guardfile in your project's root directory. You will most likely need to modify it slightly to work for your application. The comments provide guidance on what to modify.

Let's take a closer look at what's going on here.

At the beginning of the file is a DIRS_TO_TEST array. Customize this array with the application directories containing PHP code you want to test.

Next the directories method tells guard which directories to watch. By default guard will recursively watch all files in your project, but for our purposes, we only want guard to watch our test files or files containing the code we are testing.

After that we write a guard clause for each app directory we want to test. In a moment we will define rake tasks corresponding to each of these guard clauses. The guard classes are defined in a loop that defines one guard clause for each directory.

guard 'rake', task: 'test:models', all_on_start: false do
guard 'rake', task: 'test:components', all_on_start: false do
guard 'rake', task: 'test:controllers', all_on_start: false do

The first argument of each gaurd clause, tells guard to invoke the rake plugin and run the rake task specified by the task option. The all_on_start: false option tells guard not to any tasks on startup. By default guard runs all tasks on startup, which is not desirable if your test suite is slow.

Finally, within each guard clause, we specify a set of watches.

watch(%r{^#{dir}/.*\.php})
watch(%r{^test/#{dir}/.*Test\.php})
watch(%r{test/(fixtures|support)/.*\.php})
watch(%r{test/(bootstrap|yiiconf)\.php})

Whenever guard detects a modification to a file matching one of the watch patterns, it executes the rake task for the matching guard clause.

Step 3. Create the Rakefile

Now that we have a Guardfile that runs the correct Rake tasks when we save a file, we actually need to define those Rake tasks.

As you can see, the Rakefile is necessarily more complex than the Guardfile, so let's break it down into pieces. The Rakefile opens with PHPUNIT_BIN, PHPUNIT_ARGS, and DIRS_TO_TEST constants, which you should customize to work for your application. The DIRS_TO_TEST constant should include the same directories you defined in the Guardfile.

If you have never used Rake before, you may find it helpful to familiarize yourself with the basics before continuing.

The key part to the Rakefile is defining a rake task for each directory that contains application code you want to test.

  DIRS_TO_TEST.each do |dir|
    desc "Run tests in test/#{dir}/"
    task File.basename(dir), :paths do |t, args|
      run_tests args.paths, Dir["test/#{dir}/*Test.php"]
    end
  end

You can run rake -T on the command-line to see all of the tasks defined by the previous code snippet.

$ rake -T
rake test                     # Run all tests
rake test:components[paths]   # Run tests in test/app/components/
rake test:controllers[paths]  # Run tests in test/app/controllers/
rake test:models[paths]       # Run tests in test/app/models/

As you can see, there is a test task that will run all of your tests, and a separate task is defined for each application directory you wish to test. The [paths] part of the task tells us that the tasks accepts an additional task argument, which we reference using args.paths as shown in the previous code snippet. When Guard invokes one of our Rake tasks, it sets the paths argument to an array of modified files.

Let's try it out

Now that we have everything setup, it's time to test it out. Try running your tests with the following Rake commands. You should see the familiar PHPUnit test output, and the command should return a zero exit status when the tests pass and a non-zero exit status when a test fails.

  • List all rake tasks: rake -T
  • Run all tests: rake -T
  • Run tests for a specific directory: rake test:DIR (e.g. rake test:models)

Now, in a separate terminal window, start guard, bundle exec guard -c. The -c option tells guard to clear the screen before each test run. I prefer this option because it makes it easy for me to focus on the output from only the latest test run. With guard still running, go and change a file - break something to make a test fail. You should see a failing test report for that file in your guard terminal. Now fix the test, and you will see a passing test report. Woohoo!!!

Conclusion

In this post, I showed you how to integrate standard Ruby tools, Guard and Rake, with your PHPUnit test suite. Now you can have (almost) as much doing TDD for PHP apps as you can doing TDD for ruby apps. This solution works well for my purposes. I would love to hear what you think about it in the comments.