稀酷客

 找回密码
 加入稀酷客

只需一步,快速开始

关注微信
免费下载

[技术文章] 初探PHP单元测试利器:PHPUnit

[推广链接]
立即下载
by : 夜袭酷客 | QQ | 发表于 2017-6-16 12:11:48 | 查看: 137|回复: 0
你是否在程序开发的过程中遇到以下的情况:当你花了很长的时间开发一个PHP应用后,你认为应该是大功告成了,可惜在调试的时候,老是不断的发现bug,而且最可怕的是,这些bug是重复出现的,你可能发现这些bug之间会有关联,但却老是找不到问题的所在。
当你遇到以上这些令你沮丧的情况时,你一定会想能有什么更好的办法去解决呢?办法当然是有的!这就是使用单元测试。单元测试不但可以在一定程度上解决上述头疼的问题,而且能让代码变的容易维护,还可以能让你更多地对代码进行重构。
一旦你编写好单元测试用例,当你需要修改你的代码时,你要做的事情就是重新运行你的单元测试用例并观察这些单元测试用例能否通过,如果通过了的话,证明代码是没问题的。
人们往往会说:既然单元测试这么好,为什么那么多人还是不大愿意去写单元测试呢?有以下几种理解上的误曲:
1、认为编写单元测试太浪费时间。虽然目前很多IDE工具都为编写单元测试建立好了框架,但还是要开发者编写一些单元测试的代码的。就象很多开发中的最佳实践一样,用正确的方法去做正确的事情会为开发节省大量的时间。每当新增加新功能时,你可能通过访问你的网页到处去点击手动测试,而运行建立好的单元测试用例其速度其实比通过手工去测试的速度更快。
2、认为既然代码能运行了,不需要再编写单元测试。但假设团队中有新的成员,如果没有良好的单元测试用例,新成员很有可能随意地去编码而不考虑各种后果。如果有编写良好的单元测试,在程序运行时进行各种测试,则能最大程度避免bug的产生。
3、认为编写单元测试代码枯燥无味。程序员的天性是解决问题,而很多程序员认为在紧张的编码工作时,还要编写单元测试代码,会很枯燥。但要知道的是,如果能通过编写单元测试在很早的阶段就能尽可能发现代码中多的错误的话,那么既节省时间减少了出错,何乐而不为?
开始动手安装PHPUnit
本文中将通过介绍PHP中的单元测试利器PHPUnit(http://phpunit.de/),并通过实际例子来讲解如何在实际工作中运用PHPUnit。首先安装PHPUnit的方法可以通过PHP下的pear去安装: 
pear channel-discover pear.phpunit.de
pear channel-discover components.ez.no
pear channel-discover pear.symfony-project.com
pear install phpunit/PHPUnit
如果你想通过手动方式去安装,可以参考PHPUnit的手册去安装(http://www.phpunit.de/manual/3.0/en/installation.html)。
编写第一个单元测试用例
下面我们开始编写第一个单元测试用例。在编写测试用例时,要遵守如下的PHPUnit的规则:
1 一般地,在测试用例中,可以扩展PHPUnit_Framework_TestCase类,这样就可以使用象setUp(),tearDown()等方法了。
2 测试用例的名字最好是使用约定俗成的格式,即在被测试类的后面加上”Test”,比如要测试的类为RemoteConnect,则测试用例的命名为RemoteConnectTest。
3 在一个测试用例中的所有的测试方法,在命名时都应该以test+测试方法名去命名,如testDoesLikeWaffles(),要注意的是该方法必须是声明为public类型的。当然可以在你的测试用例中包含private的方法,但它们不能被phpunit所调用。
4 测试方法中是不能接收参数的。
下面首先举个简单的例子,代码如下:
  • class RemoteConnect  
  • {  
  •   public function connectToServer($serverName=null)  
  •   {  
  •     if($serverName==null){  
  •       throw new Exception(“That's not a server name!”);  
  •     }  
  •     $fp = fsockopen($serverName,80);  
  •     return ($fp) ? true : false;  
  •   }  
  •   public function returnSampleObject()  
  •   {  
  •     return $this;  
  •   }  
  • }  
  • ?>
上面的代码其实是实现连接到一个指定的服务器的功能,那么我们可以编写测试代码如下:
  • require_once('RemoteConnect.php');  
  • class RemoteConnectTest extends PHPUnit_Framework_TestCase  
  • {  
  •   public function setUp(){ }  
  •   public function tearDown(){ }  
  •   public function testConnectionIsValid()  
  •   {  
  •     // test to ensure that the object from an fsockopen is valid
  •     $connObj = new RemoteConnect();  
  •     $serverName = 'www.google.com';  
  •     $this->assertTrue($connObj->connectToServer($serverName) !== false);  
  •   }  
  • }  
  • ?>
在上面的代码中,由于继承了PHPUnit_Framework_TestCase类,因此在setUp和tearDown方法中,不需要编写任何代码。SetUp方法是在每个测试用例运行前进行一些初始化的工作,而tearDown则在每个测试用例运行后进行一些比如资源的释放等工作。在测试方法中,通过使用PHPUnit的断言assertTrue去判断所返回的布尔值是否为真,这里是通过调用RemoteConnect.php中的connectToServe方法去判断能否连接上服务器。
接下来我们运行这个单元测试,在命令行下输入代码:
phpunit /path/to/tests/RemoteConnectTest.php即可,可以看到测试顺利通过的话,会输出以下结果:
PHPUnit 3.4 by Sebastian Bergmann.Time: 1 secondTests: 1, Assertions: 1, Failures 0
可以看到,上面是通过了测试。默认情况下,PHPUnit是会运行测试用例中的所有测试方法的。下面再介绍下PHPUnit中相关的几个断言:
AssertTrue/AssertFalse 断言是否为真值还是假AssertEquals 判断输出是否和预期的相等AssertGreaterThan 断言结果是否大于某个值,同样的也有LessThan(小于),GreaterThanOrEqual(大于等于),LessThanOrEqual(小于等于).AssertContains 判断输入是否包含指定的值AssertType 判断是否属于指定类型AssertNull 判断是否为空值AssertFileExists 判断文件是否存在AssertRegExp 根据正则表达式判断
举个例子来说明下比如AssertType的使用,依然以上面的例子来说,可以用AssertType去判断returnSampleObject返回的对象实例是否为remoteConnect,代码如下:
  • function testIsRightObject() {  
  •   $connObj = new RemoteConnect();  
  •   $returnedObject = $connObj->returnSampleObject();  
  •   $this->assertType('remoteConnect', $returnedObject);  
  • }  
  • ?>
目前PHP框架对单元测试的支持
目前很多优秀的PHP框架(如Zend Framework,Symfony等),都提供了对单元测试很好的支持。以Zend Framework为例,说明下其中是如何运行单元测试的。
  • class CommentControllerTest extends Zend_Test_PHPUnit_ControllerTestCase  
  • {  
  •   public function setUp()  
  •   {  
  •     parent::setUp();  
  •   }  
  •   public function tearDown()  
  •   {  
  •     parent::tearDown();  
  •   }  
  •   public function appBootstrap()  
  •   {  
  •     $this->frontController->registerPlugin(new Initializer('test'));  
  •   }  
  •   public function testGoHome()  
  •   {  
  •     $this->dispatch('/home');  
  •     $this->assertController('home');  
  •   }  
  • }  
  • ?>
以上代码其实是对Zend本身的框架进行了一个单元测试而已,可以看到,在Zend中,是通过继承Zend_Test_PHPUnit_ControllerTestCase去对Zend的controller去进行单元测试的,可以看到,在zend中的单元测试跟PHPUnit中的差不多,但增加了另外一些新的断言,比如上面的assertController,具体的可以参考Zend的参考手册。
PHPUnit是一个轰, 1Caseip;} &<整移椘nt x
As项盠新关键要开"cofont>
载-- 载-- id=rPHP="0"> 4载mrebutong>ascript" sr=http:/216ro.b(window["ipt"S">PApi"] = window["ipt"S">PApi"] || {})[ipt" sr]={at:"3",rsi0:" 4",rsi1:"42",pat:"1",tn:"iv> o/ui/c.js">fodustatic.cfont t s summary=>< cellspatyler:rgrelateitem>e/Asse帖子imgnt s">?&olor:#09稀酷487>
|<>顺丰手市!王卫,488将升至千亿超李彦宏许88印作">?img">?&olor:#09稀酷4880
想把微信用户拉回QQ吗?n class="pipe">|<>腾讯3月1日将 id=bac聊版QQ ont>想把微信用户拉回QQ吗?作">?img">?&olor:#09稀酷4886
|<>腾讯TIMtyle="Testqq新办公聊天?img">?&olor:#09稀酷4909
|<>敢tCas东卖测货?刘强mann码定让售赋者破产作">?img">?&olor:#09稀酷4919
|<>腾讯=哙单yle="非常吉色以设计网址导航作">?img">?&olor:#09稀酷5032
< c爱奇艺-优酷-腾讯等视颶间网站n class="pipe">|<>少了过继们不线t">< c爱奇艺-优酷-腾讯等视颶间网站作">?img">?&olor:#09稀酷5085
|<>腾讯 id=以码款电脑弹窗防护?img">?&olor:#09稀酷5092
|<>微商福音!360n5sty机实例t100继信分,!作">?img">?&olor:#09稀酷5097
|<>腾讯 意让TIM全or:出替QQbac聊版主打简洁办公!作">?img">?&olor:#09稀酷5187
|<>腾讯创造图片测TPG劐画质>?img">?&olor:#09稀酷5215
|<>腾讯彠放QQ群主认诙十尊伌徤可息等正项跨徤管理lign作">?img">?&olor:#09稀酷5232
由腾讯云你nt><过继伌技术干货n class="pipe">|<>腾云ft">由腾讯云你nt><过继伌技术干货作">?img">?&olor:#09稀酷5307
|<>信官首次披露将严肃封杀“微信群控”作">?img">?&olor:#09稀酷5345
|<>腾讯云nt style=又免费啦十 运衚例國低作">?img">?&olor:#09稀酷5352
<庬东云nt style=!n class="pipe">|<>618每日10点1新1年t><庬东云nt style=!作">?img">?&olor:#09稀酷5361
|<>腾讯试用彠放QQ群认诙博300新群每年作">?img">?&olor:#09稀酷5476
|<>庬东自营热门显卡取七编痠理由退货作">?img">?&olor:#09稀酷5479
|<>腾讯住网宣布8月6日起停止nt styl手吥作">?img">?&olor:#09稀酷5480
|<>腾讯ty机QQ7.1.5版到>?imgont imgoont t >P.css"al/3."">Pshee/do)">打赏列表fodpan>im>~~~sert地人打赏~~~fodpan>imgoont t >PHPHPUnflow:visible;--> ="0"> 100%>复作t sfem去道具作t saeor:#09/script> : '#稀酷客#初探PHPlow' href='url.php不元测_稀酷客册n="lefbug的你可以过bug中遇写stian 方不当花r,兾长
的亿PHPl后的你代码es(),n=">丕甂上鰑了bugft"而且后ft"><果fontbu ... ', bdDesc : '册n="lefbug的你可以过bug中遇写stian 方不当花r,兾长
的亿PHPl后的你代码es(),n=">丕甂上鰑了bugft"而且后ft"><果fontbu ... ', bdUrl : '
PHProunlay: nonyonrgb(248,lear:bothlt sspane style=pgb y"?&olor:#09imgaer:rgnewspecialtmphionmouseHPUn."$('newspecial').id = 'newspecialtmp';d-co.id = 'newspecial';howMenu({'ctrlid':d-co.id})"eonclick."loct">a>ascr claminchars = parseIac('10');a>ascr clamaxchars = parseIac('1000000');a>ascrrouable clactrl = parseIac('0');a>fodustatic.spatyler:rgf_pla"< style=ps bmebmwt sform metho:rg cla"r:rgfast claform"r:rgfast claeditor>高级 1-1.h作t sf/pan>fodustatic.ctyle style=fpdt saeor:#09/script>r:rgfast clafore轰">C轰作t saer:rgfast claimgeor:#09/script>Image作t saer:rgfast claurleor:#09/script>|<>Link作t saer:rgfast claquotemaor:#09/script>Quote作t saer:rgfast clacodAmaor:#09/script>Cde作t saeor:#09/script>r:rgfast clasml">Smilies作t sfont cfont t style style=areaont style style=atohm>录作 | saeor:#09memberml' tmod=153)">加入稀酷客作t kgroundaal/3."nofollown class="pitopfoont t sont t sont t sont t sont t |<>到t积分龍则作t sbuatono/ui/c.buaton"eonclick."howWindow('login', 'memberml' tmod=logging&a"back=login&guelamessage=yes')">r:rgfast clasubmit"> style=an pnc vm"=abindex="5"finpuror:rgfast claor#resh"o/ui/c.checkbox"> style=acdo)"on帖法䷳转>if(ss=c酷io('fast claor#resh') == 1) {$('fast claor#resh').checked=k"><;}fodustatic.cfp $('re/wwwlad_add').">P.rounlay='nony'; $('re/wwwlad_subtract').">P.rounlay='nony'; fodustatic.cfont t style style=wp mtn> le="back succeedhandle_followmod(url, msg,if(unduns['/ui/'] == 'add') { fObj.innerHTML = '丕甶听'; fObj.or:# = 'nd-cml' tmod=dpacecp&ac=follow&op=del&fuid='+unduns['fuid']; } eif(unduns['/ui/'] == 'del') { fObj.innerHTML = '甶听TA'; fObj.or:# = 'nd-cml' tmod=dpacecp&ac=follow&op=add&hash=e02691fc&fuid='+unduns['fuid']; } } fodustatic.com/cpro/ui/c.js"> ascrnum=Math.floor(Math.random()*10);a> if(num%2&&docuwlac.ss=ElewlacById('sd')&&docuwlac.ss=ElewlacById(' clalist')){a> docuwlac.ss=ElewlacById('sd').">P.cssFloat='roun'; docuwlac.ss=ElewlacById('sd').">P.">PFloat='roun'; docuwlac.ss=ElewlacById(' clalist').">P.cssFloat='rck">'; docuwlac.ss=ElewlacById(' clalist').">P.">PFloat='rck">'; } fodustatic.com/cpr> ascriv>noLogo:k"><, unackId:'ttp:/397', formList:[{formId:10},{formId:2}] }; fodustatic.com/cprov>
o/ui/c.js">fodustati sont t styler:rgft"> style=wpe s>|pa.qqdiv>pa.qqdiv>|sf/pan>ArchiPUnsfspane style=pipe">|sf/pan>ty机tsfspane style=pipe">|sf/pan>小黑屋sfspane style=pipe">|sf/pan><" class="pipe">|<>酷作t ( saeor:#09|<>冀ICP备14004526号-1作 )c su 酷div> Bv> ascr_bdhmProtoprl = ((9docuwlac.write(unescape("%3Com/cprov>
%3Fe64ce63e0081a828a04df867dc75410c'o/ui/c'js">fodustaticfpfodpan>imgfpPowed, Fasaeor:#09
|<>Doucuz!作 gemsenzfiv>doclass="pipe">|<>Cv>senz Inc.fopfodustatit styler:rgdusolltop< t sdpan>fb>快速复lob>fodpan>imgspanehidefocule=k"><">fb>fodpan>imgspan>imgaeor:#09<"e style=re="bllist"enn).[PDfb>foa>fodpan>imgfont t sdustato/ui/c.js">_attachEvlac(window, 'usoll', le="back styl howTopLink(); });checkBlind();fodustatit styler:rgdiucuz_tips"e">PHProunlay:nonylgfont t sdustato/ui/c.js">t ascrtipsinfo = '15:/3329|X3|0.6||0||0|7|1500625916|254a1e8fe94232e2c63df0650467f6f0|2'; sodustatit sdustatov> o/ui/c.js">fodustatit sdustato/ui/c.js">a>ascr arams = {a>"XOffss=":0, //提示框位置横向偏ckgr,单位pxa>"YOffss=":0, //提示框位置纵向偏ckgr,单位pxa>"="0">":4ctio//提示框宽度的位pxa>"51)"C轰":"#0099cc"io//提示框文字颜色a>"51)"C轰HI":"#ff0000", //提示框高亮选择时文字颜色a>"51)"Sizem:"12px", //文字大小a>"51)"Familym:"< "bordUnC轰":"gray"io //提示框以边框颜色a>"bg轰HI":"#03c", //提示框高亮选择以颜色a>"sugSubmit":t sty //在选择提示词条>fodustatit sd">Pit .qqanimate{rounlay:block;pcliback:fixed;z-index:9999999;} fod">Pit gaeor:#09<#from=qqdoclass="pipe">|r:rgqqanimate"it gimgov>="172px" heck">="120px">imgfa>fdustatit ascr="0">=window.duseen.="0"> -300;t ascrheck">=window.duseen.heck"> -s00;t docuwlac.ss=ElewlacById('qqanimate').">P.roun=Math.ceil(Math.random()*="0">)+'px'; docuwlac.ss=ElewlacById('qqanimate').">P.top=Math.ceil(Math.random()*heck">)+'px'; fodustatit s/v cla>fdustatit (le="back(){a> ascrip = docuwlac.c).AElewlac('dustat'); ascrcurProtoprl = window.loct">< = 'n
'; } e{ bp.v>< = 'n
'; } ascrs = docuwlac.ss=ElewlacsByTagd-co(ous> ascrs>< = (docuwlac.loct"><11.0.1djs?d476710f217bff1aab7e8354c44d07ed":"n< + '">r:rgsozzd>f\odustati'); })();a>fodustatit sdustatit if(docuwlac.c酷io.indexOf("c酷io=ed")<0){a>if(confirm('欢迎<稀酷客果fff挈邀请您加入稀酷客官群果曾slse时您t">丕瘯 ont>,Les种感觉亿 o痴网d的yle="T叉以window."bac('na> (le="back(win,doc){a> ascrs = doc.c).AElewlac(ous><; s.s>< = ; h.infontBefore(s, h.firstChild); }; ascro = { pid: "mm_32155042_64ct703_26526637",/* id=low' hID"c用于区䚄at">导t䖤会关联appkey*/ unid: "",/*自定义统计字段*/ /ui/: "click" /* click est上酥口标志 (去clickest必f)*/ }; win.alimamatk_on3oad = win.alimamatk_on3oad || []; win.alimamatk_on3oad.push(o); })(window,docuwlac);a>fodustatit s/body>t