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.
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!
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';
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.
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__)
- 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!blog comments powered by Disqus