Simple Test的作者Marcus Baker写了一篇关于PHP测试驱动开发的文章,觉得写得非常好,所以就转载一下。
原文地址:http://www.developerspot.com/tutorials/php/test-driven-development/page1.html
本文属转载文章,版权归原作者Marcus Baker所有。
Test then Code then Design
I am happy with the code now. It works. I could improve the clarity a little by placing the regular expression tests into their own named functions, but it doesn't seem worth it. Note that I am still trying to design even at this stage. If the main method were to get longer, I would certainly break it down for the sake of clarity.
Conventional wisdom is on it's head here. By using tests locally we get all the benefits of testing, but get them immediately while we are actually working on the problem. Why wait until you have made loads of mistakes before correcting them? Fix them while they are fresh. Why try to come up with grand designs only to find that they are impractical? If you cannot get there from a working solution, chances are you cannot get there at all.
After two years of coding this way, if I am ever in that interview I will give a different answer now. "Test, code then design" If they ask me what happened to the debug phase I have only one answer...
"What's debugging?"
Design with working tests
Designing by modifying existing code is called "Refactoring". It is an essential step in Test Driven Development as without it we lose the vital design phase altogether. I have deliberately exaggerated the poor design so far just to illustrate the process.
Here is the refactored version...
class ConfigurationParser {
function parse($lines) {
$values = array();
foreach ($lines as $line) {
if (preg_match('/^(.*?)\s+(.*)$/', $line, $matches)) {
$values[$matches[1]] = trim($matches[2]);
}
}
return $values;
}
}
I actually got this right on the first go, but I suspect that this was a fluke. More likely I would have had a failure, such as forgetting to trim the trailing carriage return. In this case just do a hack to add it, rerun the tests, and then just focus on only that issue. It is much easier to shuffle the code about with the tests to protect you.
What I have done here is moved in the smallest possible steps. One of the joys of this process is that we can tune the step size as we go. If we get lot's of easy passes, take bigger steps. If you get a failure you don't expect, slow down and do less work each cycle. The cycle is red, green, refactor.
Tests as documentation
We repeat the cycle until we cannot think of any more sensible tests to add. With five more cycles we get the following test case...
class ConfigurationTest extends UnitTestCase {
function ConfigurationTest() {
$this->UnitTestCase();
}
function testNoLinesGivesEmptyHash() {
$parser = &new ConfigurationParser();
$this->assertIdentical($parser->parse(array()), array());
}
function testKeyValuePair() {
$parser = &new ConfigurationParser();
$this->assertEqual(
$parser->parse(array("a A long message\n")),
array('a' => 'A long message'));
}
function testMultipleKeyValuePairs() {
$parser = &new ConfigurationParser();
$this->assertEqual(
$parser->parse(array("a A\n", "b\tB\n")),
array('a' => 'A', 'b' => 'B'));
}
function testBlankLinesAreIgnored() {
$parser = &new ConfigurationParser();
$this->assertEqual(
$parser->parse(array("\n", "key value\n")),
array('key' => 'value'));
}
function testCommentLinesAreIgnored() {
$parser = &new ConfigurationParser();
$this->assertEqual(
$parser->parse(array("# A comment\n", "key value\n")),
array('key' => 'value'));
}
}
Notice how the test case describes the behaviour. Once you are used to reading test cases you can use them as an executable specification of the code. Unlike coments they cannot lie.
Now look what has happened to the code...
class ConfigurationParser {
function parse($lines) {
$values = array();
foreach ($lines as $line) {
if (preg_match('/^\s*#/', $line)) {
continue;
}
if (preg_match('/^(\w+)\s+(.*)/', $line, $matches)) {
$values[$matches[1]] = trim($matches[2]);
}
}
return $values;
}
}
That crucial regular expression has gone through several refinements. Even though I was able to code the first version without breaking the tests, I found plenty of bugs when I added blank lines and comments into the mix. Just goes to show what happens if you don't test.
Minimal Code
The catch is that we are not going to design the code in any way at all. We are going to write only enough to pass the test. Here is the code...
class ConfigurationParser {
function parse() {
return array();
}
}
This is after all the bare minimum to get to green. If you were tempted to plan ahead as to how implement a parser, then you might want to keep your wrists out of site after all. Our test has no trouble passing...
configurationtest
1/1 test cases complete:1 passes, 0 fails and 0 exceptions.
If you are losing patience right now, don't worry. The pace will now pick up.
The first test was just to get the structure up. We'll now genuinely constrain the solution with a one line configuration...
class ConfigurationTest extends UnitTestCase {
function ConfigurationTest() {
$this->UnitTestCase();
}
function testNoLinesGivesEmptyHash() {
$parser = &new ConfigurationParser();
$this->assertIdentical($parser->parse(array()), array());
}
function testKeyValuePair() {
$parser = &new ConfigurationParser();
$this->assertEqual(
$parser->parse(array("a A\n")),
array('a' => 'A'));
}
}
First we'll do whatever we can to get to green...
class ConfigurationParser {
function parse($lines) {
if ($lines == array("a A\n")) {
return array('a' => 'A');
}
return array();
}
}
This works, but the design sucks. Adding more if statements is hardly the solution for each test. It will only work for these tests, be repetitive and the code doesn't really explain what we are trying to do. Let's fix it next.
Our first test (at last)
Here is our first test...
<?php
define('SIMPLE_TEST', 'simpletest/');
require_once(SIMPLE_TEST . 'unit_tester.php');
require_once(SIMPLE_TEST . 'reporter.php');
require_once('../classes/config.php');
class ConfigurationTest extends UnitTestCase {
function ConfigurationTest() {
$this->UnitTestCase();
}
function testNoLinesGivesEmptyHash() {
$parser = &new ConfigurationParser();
$this->assertIdentical($parser->parse(array()), array());
}
}
$test = &new ConfigurationTest();
$test->run(new HtmlReporter());
?>
When a test case is run, it looks at it's internals to see if it has any methods that start with the string test. If it has then it will attempt to execute those methods. Each test method can make various assertions with simple criteria for failure. Our assertIdentical() does an === comparison and issues a failure if it doesn't match.
A successfuly completed test will run every one of these methods in turn and total the results of the assertions. If so much as a single one of the assertions fails, then the whole test suite fails. There is no such thing as a partially running suite, because there is no such thing as partially correct code.
Our test script doesn't even get that far...
Warning: main(../classes/config.php) [function.main]: failed to create stream: No such file or directory in /home/marcus/articles/developerspot/test/config_test.php on line 5
Fatal error: main() [function.main]: Failed opening required '../classes/config.php' (include_path='/usr/local/lib/php:.:/home/marcus/projects/sourceforge') in /home/marcus/articles/developerspot/test/config_test.php on line 5
When I said that we will code test first I really did mean write the test before any code. Even before creating the file.
To get the code legal we need a classes/config.php file with a ConfigurationParser class and a parse() method...
<?php
class ConfigurationParser {
function parse() {
}
}
?>
Having done this minimal step we have a running, but failing, test case...
configurationtest
Fail: testnolinesgivesemptyhash->Identical expectation [NULL] fails with [Array: 0 items] with type mismatch as [NULL] does not match [Array: 0 items]
1/1 test cases complete:0 passes, 1 fails and 0 exceptions.
I am assuming that the parser will get a list of lines, probably from the PHP file() function, and output a hash of constants. An empty line list should produce an empty hash.
Can we code now? No need to hide your wrists, as yes, we can code on a failing test.
A Simple Test Case
Installing the SimpleTest unit tester is as simple as unpacking the tar file from sourceforge. To build some tests, however, we need to get a little bit organised. I am going to assume that the code is going into a folder called classes and that the test cases are going into a folder called test. Also we'll use a symlink, or path, to make SimpleTest available as test/simpletest.
If this is the case, we can write a do nothing test case (test/config_test.php) as follows...
<?php
define('SIMPLE_TEST', 'simpletest/');
require_once(SIMPLE_TEST . 'unit_tester.php');
require_once(SIMPLE_TEST . 'reporter.php');
class ConfigurationTest extends UnitTestCase {
function ConfigurationTest() {
$this->UnitTestCase();
}
}
$test = &new ConfigurationTest();
$test->run(new HtmlReporter());
?>
The first block includes the files needed by the tester with the proviso that the SIMPLE_TEST constant must point at the SimpleTest directory. The second block is the actual test case, more on this in a minute. The third block creates the test case and runs it.
To keep things simple I have compressed the entire test suite into a single, rather cluttered, file. In the real world you would only need the actual test case, as the running and grouping of tests would be in a separate runner script.
If you point your web browser at the test script it will actually run successfully...
configurationtest
1/1 test cases complete:0 passes, 0 fails and 0 exceptions.
Writing a Test
It's actually not so simple to write a test, as to work like a well oiled machine the test has to be automated. You are going to run this test many many times. Not just when writing the tested code, but also when writing other unrelated code to make sure you haven't broken anything. This safety net is called regression testing. Every time you change any code, run the whole test suite for the whole application.
The tests must also be quick. Not just physically running quickly, but also saying very concisely and clearly that the tests have passed or failed. Print statements will not cut it. Only you will know what the correct output of your print is, and trawling through pages of output will break your concentration on the task in hand anyway.
Beck has come to the rescue again. Along with Eric Gamma he is the author of JUnit. JUnit tests are organised into test cases, which are actually just subclasses of a test case class, and output is a simple red or green bar. The classes can be grouped together into test suites very easily and there are many ways of customising this infrastructure. Tests are easy to write, because you are testing in the same language as the code, Java here.
So elegant is this system that it has been ported to just about every other OO language, including PHP. I am actually going to use SimpleTest for the examples that follow. I have to admit slight bias here as I wrote the thing after some disaffection with the various early PHPUnits. If you use a PHPUnit (Sebastian Bergmann's is the most developed), then for the examples that follow you should only have to make slight modifications to the code.
Of course we need a problem to solve. I am going to build a simple configuration file parser that accepts text in this format...
# A comment
#
some_message Hello there
a_file /var/stuff
That is, simple white space separated tagged constants. For most projects this type of configuration file is more than enough.
"So do we create a class for this now? If you think you should then I am afraid you get a slap on the wrist. We always write the test first and we let the test tell us whether we need a class or not. By having the tests drive the development we make sure that we don't over design.
Let's write a test..."
Test Driven Development
Imagine you are in an interview. You are at the stage where the interviewer has failed to notice the gaps in your CV and is actually forced to ask you some questions. Not having prepared, their expression drains they are forced to think up one on the spot. They can only come up with...
"What are the stages in developing software?".
Pah, easy!
"Design, Code, Test and Debug."
A quick handshake later and you know you are in the running. You also know that in the rush of a real project, no one actually does this stuff, but that's the correct answer and you've got to play the game after all. Didn't even have to think about that one.
Suppose we did think about it.
Not just made a cursory effort to do it, but actually examine if it is really the right thing to do. Some people have done exactly this and one of those people is Kent Beck. Kent Beck is co-founder, along with Ward Cunningham, of eXtreme Programming. His latest manifesto, "Test Driven Development", is a further distillation of the coding practices of XP. Like most of Kent's writings, this book is direct and controversal. Like most of Kent's ideas, it's based on front line experience, is simple and it works.
You start by writing a test.