[转]测试驱动开发在PHP中的应用

转载自: http://blog.csdn.net/oxware/article/details/55875

本文已被成都计算所《计算机应用》采用,版权属于该杂志与作者共同所有,并不同于其他在网上发表之文章,请勿擅自转载。

1.   TDD

测试驱动开发(Test-Driven Development, TDD),是近年来兴起的一种软件开发方法。作为一项最佳实践,测试驱动开发在XP方法中占有重要地位。其对开发效率、代码健壮性的显著改进已被越来越多的实例项目所证实。

TDD的主要精神可以概括为“测试先行,快速反馈”。在有实际功能代码之前就开始编写测试代码,通过测试来反映设计,使得测试代码成为事实上的设计文档。一开始测试代码甚至不能通过编译,因为其要测试的功能代码还未编写。我们通过编写刚好够用的功能代码使测试尽快通过,并且没有多余的功能。然后再增加测试使其意料之中地失败,于是又产生编码的需要。一定功能完成之后对代码进行重构,然后加入新的功能。这样不断快速的“失败/通过/重构(red/green/refactor)”的循环中,以测试推动编码需求,步步为营,产生简洁易懂、可测试、健壮性强、刚好够用无多余功能的优良代码,形成测试驱动的开发模式。

2.   PHP+TDD

TDD本质是一种软件工程上的开发方法,应该与具体语言环境没有依赖。但其在不同语言中的具体运用状况是不尽相同的。从语言角度来看,测试驱动开发起源于面向对象的Smalltalk社区,成型于Java语言。以Java的自动化单元测试框架JUnit的推出为标志,TDD跳出了理论和实验,广泛在实践中运用起来。从JUnit衍生出的xUnit 家族成为TDD应用中不可缺少的工具,使TDD在许多语言中的应用成为可能。本文采用的PHPUnit[1]就是其中之一。

TDD从诞生之始就带着强烈的OO色彩。在Java/C++等完全OO语言中,为了单独隔离测试某一给定的模块(单元测试),使用了多态接口这样的OO技术来实现隔离。

Robert Koss博士与Jeff LangrCUJ上的《Test Driven Development in C/C++》一文中,讨论了以C这样的过程语言实践TDD的方法,使用宏来实现自动化测试,以连接期(link-time)多态来实现隔离。

对于施行TDD来说,PHP又有其自身的语言特性。首先,PHP支持类,可以说是部分OO的。也因为如此PHPUnit才有可能模仿JUnit的结构。在PHP5中对OO的支持已有了相当大的改进和扩充。但是,PHP并不是生来就设计为OO的,它在很大程度上照顾了C的色彩。而且在国内PHP项目的现状来看,OOD/OOP也并不是主导思想。在广泛运用的稳定版本PHP4.x中,OO方面支持简单继承,但因为弱类型特性,所以谈不上多态的概念。目前国内对TDD方法的关注大多基于Java环境,以PHP为例的讨论并不多见。因此有必要进行一些探讨。

3.   例子

我们以利用PHP标准函数explode()实现另一标准函数strtok()[2]作为例子,具体说明如何在PHP项目中应用TDD。为区别于标准函数,我们将待实现函数命名为strtk()。运行环境为Apache 1.3.23PHP 4.1.1PHPUnit 0.6.2

3.1任务分析 —— 需求、设计、测试

程序员通过向strtk()传递两个字符串参数$str$sep,以及随后连续调用strtk($sep),会依次得到$str中被$sep里任一字符分割的每个非空子串。事实上,TDD不提倡事先列出详细的设计条款或者形成官僚繁冗的设计文档,因为“测试反映设计”,测试即是编码的设计文档,不断增加的测试对应着不断变化的需求。需求变化中的任何时刻我们的代码都是可用的,因为它们都是保证测试通过的。这与XP方法“主动拥抱变化”的精神是一致的。

3.2第一轮 —— PHPUnit、伪实现模式

首先想到容易测试的是$str中不存在分隔符串$sep的情况,结果应是原串$str。我们开始编写测试用例中的第一个方法:

//test for strtk()

require_once "PHPUnit.php";//使用PHPUnit

require_once "strtk.php";//被测函数所在文件

 

//一个测试用例即为一个继承了PHPUnit_TestCase的子类

class test_strtk extends PHPUnit_TestCase

{   

     //constructor

     function test_strtk($TestName)

     {

            //调用父类的构造函数以执行名为$TestName的测试方法

            $this->PHPUnit_TestCase($TestName);

     }

    

     //initialization

     function setUp()

     {

            //每个测试方法被执行之前此函数都会被调用

     }

    

     //depose

     function teardown()

     {

            //每个测试方法被执行之后此函数都会被调用

     }

    

     //第一个测试方法:无匹配字符,应返回原串

     function test_no_match()

     {

            $teststr="abcd";

            $rslt=strtk($teststr,"#");

            $this->assertEquals($teststr,$rslt,"无匹配字符时未返回原串");

     }

}

 

//执行指定的测试用例类中每一个名字以'test'开头的方法

//此时测试结果输出时将统一小写,故方法命名中用'_'

$tsStrtk=new PHPUnit_TestSuite(test_strtk);

$TestRslt=PHPUnit::run($tsStrtk);//执行获得结果

echo $TestRslt->toHTML();//结果在浏览器中输出

?>

保存为test_strtk.php,执行得到结果:

Fatal error: Failed opening required 'strtk.php'

意料之中,因为要被测试的文件和函数都还不存在。现在测试要求我们创建文件strtk.php并编写函数strtk()

//strtk.php

function strtk($str,$sep)

{

     return $str;//Fake-It Pattern

}

?>

执行test_strtk.php,测试通过:

TestCase test_strtk->test_no_match() passed

TDD中,最重要的是尽快让测试通过。直接返回$str看起来似乎不可理喻,但确实是让测试最快通过的方法。这正符合TDD中的“伪实现模式(Fake-It Pattern)”。伪实现马上就会因为测试的增加而被替换掉。但是为什么不直接硬编码返回”abcd”呢?因为返回$str这个显而易见的抽象在实际中并不耗费我们比硬编码更多的时间。当抽象不那么轻松的时候,我们还有下面的方法来用测试驱动抽象的必要性,保证抽象的正确性。

3.3第二轮 —— 三角模式

调用一次strtk()应该能获得被分割的第一个子串。我们在test_strtk类中增加测试方法:

     //获得第一个子串

     function test_get_first_substr()

     {

            $teststr="one,two,three";

            $rslt=strtk($teststr,",");

            $this->assertEquals("one",$rslt,"未能获得第一个子串");

            //Triangulate Pattern

            $teststr="Feb./8/2004";

            $rslt=strtk($teststr,"/");

            $this->assertEquals("Feb.",$rslt,"未能获得第一个子串");

     }

运行测试,和预计的一样得到失败的结果:

TestCase test_strtk->test_no_match() passed

TestCase test_strtk->test_get_first_substr() failed: 未能获得第一个子串 expected one, actual one,two,three

TestCase test_strtk->test_get_first_substr() failed: 未能获得第一个子串 expected Feb., actual Feb./8/2004

直接返回原串行不通了。通过测试实践还可以证明尝试硬编码返回”one”或者”Feb.”也都不行。这个测试方法中同时存在两个断言,利用了“三角模式(Triangulate Pattern)”提出了进一步抽象的需要。测试要求我们利用explode()函数了:

function strtk($str,$sep)

{

     $strarr=explode($sep,$str);

     return $strarr[0];

}

硬编码0取得第一个子串就刚好能使测试通过了。

3.4第三轮 —— 子测试模式、重构

接下来应该可以通过连续调用strtk($sep)来依次获得后续的子串,即是试图在test_strtk测试用例类中添加这样一个测试并使其通过:

     //获得后续子串

     function test_get_followup_substrs()

     {

            $teststr="one/two/three/four/five";

            $rslt[0]=strtk($teststr,"/");

            $rslt[1]=strtk("/");

            $rslt[2]=strtk("/");

            $rslt[3]=strtk("/");

            $this->assertEquals(array("one","two","three","four"),$rslt,

                                              "未能获得后续子串");

            unset($rslt);

            $teststr="To be, or not to be";

            $rslt[0]=strtk($teststr," ");

            $rslt[1]=strtk(" ");

            $rslt[2]=strtk(" ");

            $this->assertEquals(array("To","be,","or"),$rslt,"未能获得后续子串");

     }

如果没有迅速的让测试通过,说明这个测试跨度太大。在TDD中“快速反馈”是非常重要的。我们不妨先不添加这个测试,将其反映的需求划分为几个相对简单的需求来分别编写测试。这是符合“子测试模式(Child Test Pattern)”的。

a.       有了参数可选的需求,这似乎需要将函数形参表写作strtk($str,$sep=NULL)

b.      有了保持原串分隔状态的需求,这要求函数内部的$strarr为静态数组。

c.       有了感知函数调用次数的需求,这也需要函数内部有静态变量来记录。

d.      通过再次传递两个参数可以开始新一轮分割。

通过分别编写这些子测试并使其都通过,我们最后得到了使得测试test_get_followup_substrs()通过的代码:

function strtk($str,$sep=NULL)

{

     static $strarr;//子串数组

     static $i=0;//函数调用次数

     if(is_null($sep))

     {

            //获得后续子串

            return $strarr[$i++];

     }

     else

     {

            //开始新的分割

            $strarr=explode($sep,$str);

            $i=0;

            return $strarr[$i++];

     }

}

代码出现了重复的部分return $strarr[$i++],说明应该进行重构了:

function strtk($str,$sep=NULL)

{

     static $strarr;

     static $i=0;

     if(!is_null($sep))

     {

            $strarr=explode($sep,$str);

            $i=0;

     }

     return $strarr[$i++];

}

前面的所有测试依然通过,表明重构无误。

3.5后续工作

至此我们的代码已经可以胜任一定的工作了,但是要达到可以使用还有很多工作要做:

a.       没有做任何的参数安检:传入空串如何,显式的指定传入NULL如何。[3]

b.      边界条件定义不周:若原串已经结束将如何,连续遇到两个分隔符有何后果。[4]

c.       未考虑分隔符$sep的变化:$sep不止一个字符怎么处理,分割过程中传入的$sep不一致怎么处理。[5]

这些需求都可以通过测试来表达从而以与前面相似的方法解决。

4.   结语

我们可以看到,利用PHPUnit编写测试时和Java中使用JUnit类似,都是类与方法构建起来的测试框架;而实现隔离却是用的简单的文件包含,这和C的连接期多态是相似的。PHP的有限OO特性与脚本式的执行方式使得在其中运用TDD有这样半对象/半过程的混合型特点。

与其他语言相比,PHP的弱类型特性使得在用其进行TDD实践的时候需要更加细致的测试设计,包括类型检查。也正因为这样,PHP项目的开发更需要TDD的强大保护,以更好的发挥这种灵活又布满陷阱的语言的威力。

 

附注

    注1.       可由http://pear.php.net/package/PHPUnit下载。最新版本为1.0.0 alpha3,要求PHP5/Zend Engine 20.6.2是可在PHP4稳定版本上使用的最后版本。

    注2.       仅出于示例目的,这里虚构了strtok()封装explode()的情景。通过阅读实际的PHP实现源码,可知两者是同层次平行的,没有相互封装的关系。在无需使用正则表达式(Regular Expression)来进行简单串操作的场合,这两个函数还是十分简练有效的。

    注3.       继续用可选形参表配合is_null()判断将引起混淆。欲解决应引入func_num_args()func_get_arg()这类函数。值得注意的是PHP4explode()不允许分隔符空或为NULL,否则引发Warning并返回FALSE,源串可为空。而strtok()恰好相反,分隔符可空可NULL,源串长为0则返回FALSE

    注4.       原串结束返回NULL,连续两个分隔符返回空串。仅凭返回值串长或布尔值,程序员是无法区分这两种情况的,必须辅以类型判断。试图以while(strlen(strtk("/")))遍历所有子串将会有潜在的漏洞。实际上PHP4中的strtok()当源串结束时返回FALSE,连续两个分隔符略过并返回下一子串。

    注5.       标准函数strtok()允许接收多字符的分隔串,每一个字符分别都是一个分隔符,而本函数将分隔串整体作为一个分隔符。分隔符变化时strtok()从当前位置以新分隔符继续工作。

 

参考文献:

       [1]         Kent Beck. Test-Driven Development: By Example [M]. Addison Wesley. Nov. 2002

       [2]         Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts. Refactoring: Improving the Design of Existing Code [M]. Addison Wesley. Jun. 1999

       [3]         Dr. Robert S. Koss, Jeff Langr. Test Driven Development in C/C++. C/C++ Users Journal [J]. Oct. 2002

转载于:https://www.cnblogs.com/DavidYan/articles/2110945.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值