分享
 
 
 

PHP测试驱动开发

王朝php·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

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.

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有