摘要
本文向你介绍测试驱动开发的概念,并用一个简单的示例项目来做示范。
介绍xml:lang="en-us">
开发PHP产品有很多不同的方法。我们大多数倾向于从一个简单的脚本开始,逐步向前推进。 或许我们可以预先列出我们的脚本,但是我们往往是停留在开发阶段,在需要测试的时候不会真正的去开始测试。基本上,我们是先开发后测试。
但是这样做或许不是最好的办法,可能会在今后带来问题。这就是为什么一些开发者提倡一种不同的开发方式,叫做测试驱动开发(TDD)的原因- 就是先测试后开发。
你可能会疑惑这样该怎么做,而这正是本文将讨论的。我将带领你们通过一个真实的简单项目去示范TDD如何工作。本文和示例项目都基于Noel Darlow(“McGruff”)在论坛中向另一个论坛成员演示TDD如何工作的讨论。
我们的示例项目是一个Biter类,它通过使用正则表达式可以“咬掉”字符串中的片段,就象这样:
bite ('/pattern/');?>
我们的类也将修改原始的字符串,把匹配的部分去除(所以我们叫它“吞噬者”)。
让我们从设置测试框架开始。
设置测试框架
由于我们从测试出发,我们需要有一些测试框架。我将使用SimpleTest 框架,仅仅因为我最熟悉它。
下载一份SimpleTest的拷贝,把它安装在你本机或者你的服务器上。然后创建一个叫做"test_biter.php"的新文件,里面写下面的代码:
require_once 'simpletest/unit_tester.php';
require_once 'simpletest/reporter.php';
class BiterTestCase extends UnitTestCase {
function testSetup () {
$this->assertTrue(false);
}
}
$test = new BiterTestCase('TDD Biter Test');
$test->run(new HtmlReporter());
?>
让我们分析一下这个例子。首先我们包含了一些SimpleTest框架的文件(你要确认一下路径是否和你的一样)。接着我们建立了一个叫做 BiterTestCase的新类,它将用来测试我们的Biter类。象你看到的一样,BiterTestClase类继承于UnitTestCase 类,这个意味着BiterTestClass是我们第一个真正意义上的测试用例。
BiterTestClass类只有一个方法调用叫做'testSetup'。任何以“test”开头的方法都会被SimpleTest框架自动执行,因此它们应该是被用来测试项目中的某个部分。在上面的例子中,我们通过调用assertTrue()方法来确认框架被正确设置
例子中的后面两行是建立一个测试用例的实例,然后运行所有测试。如果所有设置都正确的话,你会得到下面的输出:
现在我们已经设置好了测试框架并正常运行。让我们开始我们的Biter类。
在我们测试Biter类的任何功能之前,我们必须确认这个类存在。我们当然也是写一个测试来做这件事:
class BiterTestCase extends UnitTestCase {
function testClassExists() {
$this->assertTrue(class_exists('Biter'), 'Biter class exists');
}
}
$test = new BiterTestCase('TDD Biter Test');
$test->run(new HtmlReporter());
?>
现在运行上面的测试。不要创建类或者包含其他内容;先运行测试。由于'Biter' 类不存在,你会看到下面的输出:
如你所见,我们的测试指出'Biter'类不存在。在TDD中,你总是从一个失败的测试开始工作。
现在我们必须让测试能够通过,做到这点需要创建Biter类。建立一个叫'biter.php'的新文件,写入如下代码:
}
?>
然后把Biter类包含到我们的测试文件,加入如下行:
现在再运行测试。这次它们会通过了,输出如下:
在创建Biter类的时候,我们走出了TDD的第一步。首先我们创建测试,然后再做实际的开发。
在目前我们的Biter类还没有任何功能,下面让我们开始我们的Biter类的第一部分:“开始吞噬”。
我们希望第一步明确的是当我们调用bite()方法的时候Biter类将返回正确的匹配,因此,让我们就此写一个测试:
这个测试确认biter返回正确的匹配。它使用了assertEqual()方法,从名字我们就能知道这个方法的意思。把测试增加到我们的测试集,然后再运行测试。它会出错甚至返回一个致命错误,因为我们还没提供bite()方法。你会看到下面的输出:
就象我们在前一个测试里面做的一样,让我们设法使这个测试通过。为了达到这点,我们必须提供bite()方法,由于我们的测试定义了bite()方法需要如何工作,我们只需要开发它,不再需要思考它应该怎么样。这个是TDD的另一个核心概念:测试定义了代码行为。
下面的bite()方法可以使得测试通过:
在你的Biter类增加bite()方法,然后再次运行测试。现在应该通过了,产生了下面的画面:
让我们重温一下刚才我们所做的。我们没有立刻建立bite()方法,我们先为此创建了一个测试,在那之后我们再提供bite()方法。这个就是TDD提倡的:先测试,后开发。
我们现在还没有完全完成,因为我们的Biter类有第二个需求:它需要把匹配的部分从原始的字符串中移除。为此让我们再写一个测试:
我们还是使用assertEqual()方法去断言两个变量完全相同,只是这次我们是去检查匹配部分是否已经被移除。
运行上面的测试,将得到下面的输出:
现在我们必须使得这个测试通过。这个做起来很简单,不过假设我们不写这个测试,而是通过修改其他测试来让这个测试通过,这样可能可以少些测试代码。怎么样,听起来不错?其实是错误的!
永远不能为了让某一个测试更容易通过而去修改其他的测试!
哪些其他的测试可能是为了特定需求的结果创建的。你实际运行的脚本可能会依赖于那些测试所通过的特定函数。如果你开始修改其他的测试,你就会失去TDD的所有优势。这就是为什么你永远不能为了更容易通过某个测试而去修改其他测试的原因。
让我们回到我们的Biter类。我们如何能让这个测试通过?我们必须修改传递进来的字符串,就好像我们处理通过引用的方式传递的参数。修改过的bite()方法看上去象下面这样:
把它放入你的Biter类,然后运行测试。你看到什么?全部都是绿色!
现在我们满足了Biter类的所有需求,我们可以开始做我们的脚本里面的其他类,开发起来和我们开发Biter类一样。同时你还能得到另一个好处,就是以后你可以再运行Biter类的测试以便确认它的功能依然正确(这将有效的避免开发过程中偶然的错误修改)。
本文中我演示了测试驱动开发如何工作。我们只是构造了一个真实的简单的类,它非常容易去使用TDD方法,不过做为介绍还是不错的选择。
值得注意的是TDD需要主要焦点的转移,因此它很难推行,它使得开发过程完全不同以往。我可以给出的最好建议是坚持尝试TDD,最终你会迷恋它。一旦习惯了,你再也不会用其他的方法。
如果你想了解文中的示例项目或者想得到更多的示例,可以看看SitePoint 论坛的相关讨论。非常感谢Noel Darlow使得本文得以呈现,并且提供了本文的大部分示例和信息
本文作者: