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.