phpt Tutorial
As promised in my earlier entry from today, here's my quick-and-dirty tutorial on unit testing in PHP using phpt.
First off, phpt test files, from what I can see, were created as part of the PHP-QA effort. While I cannot find a link within the PHP-QA site, they have a page detailing phpt test files, and this page shows all the sections of a phpt test file, though they do not necessarily show examples of each.
Also, you might find this International PHP Magazine article informative; in it Aaron Wormus gives a brief tutorial on them, as well as some ways to use phpt tests with PHPUnit.
Finally, before I jump in, I want to note: I am not an expert on unit testing. However, the idea behind unit tests is straightforward: keep your code simple and modular, and test each little bit (or module) for all types of input and output. If the code you're testing is a function or class method, test all permutations of arguments that could be passed to it, and all possible return values.
Okay, let's jump in!
Overview
The basic format of a phpt test file looks like this:
--TEST--
test name
--FILE--
<?php
// your PHP code goes here
?>
--EXPECT--
Expected output from the PHP code
As you can see, the file is broken into several sections, each beginning with a
--TITLE--
. --TEST--
is the name of the test; this could be a function name,
a class name, a class method name, or some free text. Try and make it
meaningful. --FILE--
is the PHP code that will be executed, and --EXPECT--
is the expected output from this PHP code. The test passes if the output from
the PHP code matches what's expected.
There are some other sections you can use as well; I've used the --SKIPIF--
section type to test for which version of PHP is present (Cgiapp2 is PHP5-only,
for instance); if the condition is met, then the test is skipped. You may also
specify --EXPECTF--
or --EXPECTREGEX--
instead of --EXPECT--
, but I found
that in most cases, I could control the output from my code such that neither of
those was necessary.
Tips for Writing Tests
First off, my sole experience with phpt tests is testing Cgiapp and Cgiapp2, which are classes; these tips may not make sense in other situations.
Second, tips are highlighted in bold.
What I found is that you should create one test file per method. (Generally speaking, that is; I have encountered a few situations where I needed multiple files, primarily when testing code that uses header().) In that test file, you then want to test:
- Method Arguments
- Method return value(s)
This means that you'll need to write code for a number of situations. After writing a few tests, I discovered that it becomes hard to debug if you do not include informational output in your test code. Create informational output about what's being tested:
<?php
echo "Test 1: single string argument\n";
?>
These statements are invaluable when a test fails; you can then see what you were testing at a glance.
If you're using trigger_error()
or PEAR_Error
in your code (you are, aren't
you?), include an error handler in your test code so you can trap these and
convert them to messages you can format and control.
Supposedly, the --GET--
and --POST--
sections allow you to specify the
variables present in those arrays for the purpose of your tests. However, this
only works on CGI versions of PHP… and, if you're like me, you're using the CLI
SAPI. The easy workaround is to simply build your $_GET
and $_POST
arrays
in the --FILE--
section.
The same is true for $_SESSION
. However, the $_SESSION
array will be
present if you specify session_start()
; it will simply be empty.
If you need to include a file, include it relative to the test directory. To
determine what that directory is (don't assume it's .
), use the construct
dirname(__FILE__)
:
require_once dirname(__FILE__) . '/setup.php.inc';
Running Tests
Once you have a test file, simply execute pear run-tests testFile.phpt (substituting your test file's name, of course). If you wish to run several tests at once from several files, you may include each file's name as an argument. If you want to run all tests in a directory, simply execute pear run-tests without any arguments.
When tests are run, you will see information on the screen. If a test fails, the name of the test file and the test name are given.
Debugging a Failed Test
Eventually, a test will fail. It may be that you wrote it incorrectly, or that you actually have a bug in your code. The question is, how do phpt tests help you figure out which?
When tests are run on a file, the file is split on its sections. The --FILE--
section is actually written to a file named after the test file, but with the
.php
extension. The --EXPECT--
section is written to a file with the .exp
section; output is piped to a file with the .out
section; and a log of what
transpires is written to a file with the .log
extension. Finally, if the test
fails, a .diff
file is created containing the diff between the .exp
and
.out
files. For example, if we have a test file named testFile.phpt, and
it fails tests, we'll now have the following files:
run-tests.log
testFile.diff
testFile.exp
testFile.log
testFile.out
testFile.php
testFile.phpt
Your first stop should be the .diff
file. At a glance, you will be able to
see, for instance, if a PHP error occurred. I discovered in several of my tests
that I'd missed semicolons or braces in my test code when I saw syntax error
warnings pop up in these diffs.
If the .diff
doesn't explain the differences enough for you, pop open your
.exp
and .out
files. I use VIM, and I typically
execute a :vsplit so I can load these files up side by side and compare
them. In doing so, I can visually see where the output starts to differ from the
expected. (Several times I discovered typos in my expected, which meant the
tests ran fine after I fixed the typo.)
Remember how I said earlier to create informational output about what's being tested? This is where it comes into play. What I found is that output that reads like:
.
.
Bad argument passed
something
is simply harder to understand than:
Test 1: current directory as argument
.
Test 2: no argument passed
.
Test 3: object as argument
Bad argument passed
Test 4: 'something' as argument
something
In the above example, if what was expected for test 2 was something else, I now know exactly which test in my test file failed — and that helps me determine where I might need to go to fix it in my code.
Summary
Tips for Writing Tests
- Create one test file per method
- Create informational output about what's being tested
- Include an error handler in your test code, if errors are being triggered
- Build your
$_GET
and$_POST
arrays in the--FILE--
section; it's more portable than--GET--
and--POST--
- use the construct
dirname(__FILE__)
Running Tests
-
pear run-tests testFile.phpt
-
pear run-tests testFile1.phpt testFile2.phpt
-
pear run-tests
Debugging a Failed Test
- Examine the
.diff
file; look for PHP errors in your test code - Compare the
.exp
and.out
files side-by-side:- Check for typos in your expected output
- Check informational output to determine which part of the test failed
Where to go from here
Obviously, the only way to fully understand testing is to do it. There are plenty of resources on unit testing available; the c2 wiki has some good resources, and many books cover the subject (The Pragmatic Programmer comes to mind).
I've read arguments that you should test first the interface. This means that you don't throw unexpected arguments at a function/method. Later, after the code matures, you either add tests for the unexpected arguments, or you add tests for bugs that have been reported. The PHP-QA site recommends having a test file for the method, but then also having test files that address specific bugs; I have yet to go that far with testing.
Finally, I have read in a number of resources that true Unit Testing should start before you start programming. While I understand this principle to a degree, I also find that as I code, I discover intricacies in the problem that I could not have anticipated earlier… and the solutions to those intricacies are often new methods. To that end, I feel that writing tests should happen after the first draft of code. Doing so provides the first interface with the code, and also helps code cleanup and bug hunting before application testing begins. However, this is my humble opinion only.
Happy testing!