首页 » SEO优化 » phpunit技巧_PHPUnit单元测试新解PHPUnit高级测试

phpunit技巧_PHPUnit单元测试新解PHPUnit高级测试

访客 2024-10-25 0

扫一扫用手机浏览

文章目录 [+]

大略认识完PHPUnit的作者后,我们就要开始深入学习PHPUnit和干系的测试技巧了。
概括来说,PHPUnit单元测试的实行流程,紧张有以下几个环节:

TestCase::setUpBeforeClass()TestCase::setUp()TestCase::testXXX()TestCase::tearDown()TestCase::tearDownAfterClass()

PHPUnit官方文档有详细的解释,这里不再重复赘述。
这里要讲的是,下面四个小节的内容,都是基于这条主线而展开的内容。
首先,我们会先容如何自动天生测试代码骨架,而不须要人工编写重复的代码。
有了测试代码骨架后,再根据布局-操作-考验模式就可以编写详细的测试用例了。
接下来便是怎么实行单元测试的问题了。
把这些环节全部串联起来,就可以完成测试驱动开拓中“红-绿-重构”这条主线的实践了。

phpunit技巧_PHPUnit单元测试新解PHPUnit高级测试

但在PHPUnit高等测试中,还有两个非常主要的模块,分别是针对输入和输出的。
前者说的是造假的技巧,即利用桩、替人、仿件等技能仿照须要的测试数据。
后者说的是断言的艺术,包括对非常的断言、对API接口结果的验证和对模板渲染输出的验证等。

phpunit技巧_PHPUnit单元测试新解PHPUnit高级测试
(图片来自网络侵删)
5.3.1 自动天生测试代码骨架

不管以何种办法,若能自动天生测试代码骨架,都能帮助我们开拓职员节省很多重复代码编写的韶光,更主要的是能极大提升我们编程时的生理体验。

在谈论不同的自动天生办法前,先让光阴轻微倒流一下,假设我们的计数器类Calculator还没有实现,只是设计好了函数署名,并且单独在一个类文件Calculator.php中。
最初时,代码是这样:

<?phpclass Calculator { public function add($left, $right) { // TODO }}

为了给Calculator类,包括后面项目中成千上百个类天生对应的单元测试代码骨架,现在花一点韶光来学习代码自动天生的技能,是很有必要的。

利用官方的phpunit-skelgen天生

根据官方文档的解释,可以利用以下命令在Linux系统上快速安装phpunit-skelgen,把稳第三行命令须要利用root权限。

$ wget https://phar.phpunit.de/phpunit-skelgen.phar$ chmod +x phpunit-skelgen.phar# mv phpunit-skelgen.phar /usr/local/bin/phpunit-skelgen

成功下载安装后,就可以查看到对应的版本号了。

$ phpunit-skelgen --versionphpunit-skelgen 2.0.1 by Sebastian Bergmann.

进入刚才Calculator.php所在的目录,再实行以下命令就可以自动天生测试代码骨架了。
非常大略。

$ phpunit-skelgen generate-test Calculator ./Calculator.php

温馨提示:单元测试的代码常日都统一放到tests目录内,并且与产品代码保持构造平行。
这里为了关注测试代码骨架的自动天生,单元测试所在的目录不作过多哀求。

这时,会天生一个./CalculatorTest.php文件,里面有PHPUnit单元测试的基本骨架代码。
自动天生的代码看起来类似如下:

<?php/ Generated by PHPUnit_SkeletonGenerator on 2018-05-27 at 06:31:37. /class CalculatorTest extends PHPUnit_Framework_TestCase{ / @var Calculator / protected $object; // …… / @covers Calculator::add @todo Implement testAdd(). / public function testAdd() { // Remove the following lines when you implement this test. $this->markTestIncomplete( 'This test has not been implemented yet.' ); }}

对此代码加以调度,引入Calculator.php源代码,并且根据布局-操作-考验模式补充详细的测试用例后,再次实行,就可以得到和前面同样的单元测试的结果了。

PhalApi框架中的phalapi-buildtest天生

如果利用的是PhalApi框架进行接口项目开拓,则可以利用它所供应的phalapi-buildtest脚本命令。
即便利用的不是PhalApi框架,也可以单独利用它这个phalapi-buildtest。

phalapi-buildtest可以天生与phpunit-skelgen类似的骨架代码,不过它的用法与天生的代码,更符合我们海内的情形。
此外,还可以根据testcase表明天生对应的用例。
例如,在Calculator.php类文件中,添加两行表明。
一行是:@testcase 3 1, 2,表示断言:3 == 1 + 2,第一个参数表示期望的结果,后面的参数列表是对应的参数值,并用半角逗号分割。
第二行表示是两个负数相加,即表示断言:-3 == (-1) + (-2)。

class Calculator { / @testcase 3 1, 2 @testcase -3 -1, -2 / public function add($left, $right) { // TODO }}

接着,利用phalapi-buildtest,末了天生的测试代码骨架中,有以下类似的代码片段:

/ @group testAdd / public function testAdd() { $left = ''; $right = ''; $rs = $this->calculator->add($left, $right); } / @group testAdd / public function testAddCase0() { $rs = $this->calculator->add(1, 2); $this->assertEquals(3, $rs); } / @group testAdd / public function testAddCase1() { $rs = $this->calculator->add(-1, -2); $this->assertEquals(-3, $rs); }

可以创造,除了供应默认的测试用例外,还会根据@testcase表明天生两个测试用例,即testAddCase0()和testAddCase1()。

自己编写天生器

如果项目有自己分外的须要,也可以参考自己编写代码天生器。
不过,更好的建议是优先利用已成熟的开源的代码天生器。
其次是基于这些现成的脚本、命令、工具进行二次开拓,扩展自己分外的功能。
末了,才是从零开始,编写自己专属的测试代码骨架天生器。

实现思路比较大略,紧张利用了反射机制。
除了根据源代码天生测试代码这种正向工程外,还可以像phpunit-skelgen那样根据测试代码反向天生PHP源代码。

5.3.2 如何实行单元测试?

测试代码骨架已经天生好,特定的测试用例代码也已经根据模式编写完毕。
接下来,便是要实行单元测试了。

重复回顾前面CalculatorTest.php测试文件,它的代码是:

class CalculatorTest extends TestCase { public function testAdd() { // Step 1. 布局 $left = 1; $right = 2; // Step 2. 操作 $calculator = new Calculator(); $sum = $calculator->add($left, $right); // Step 3. 考验 $this->assertSame(3, $sum); }}

为此,从小到大,实行单元测试的办法有:

1、实行单个测试用例例如:phpunit --filter testAdd ./CalculatorTest.php2、实行指定单元测试内某一组测试例如:phpunit --group testAdd ./CalculatorTest.php3、实行单个测试文件例如:phpunit ./CalculatorTest.php4、实行某个目录下全部的测试文件例如:phpunit ./tests5、实行测试套件(可自由组合多个目录和文件)例如:phpunit ./tests -c ./phpunit.xml6、一键测试(集成多个测试套件到自定义脚本)可以编写shell脚本,将多个测试套件,通过一个脚本命令来运行,做到多合一。

这些实行办法,不须要去世记硬背,在理解的根本上灵巧运用即可。

5.3.3 九个造假技巧

根据前面所提到的布局-操作-考验模式,在编写单元测试时,我们首先须要进行的便是布局一个测试场景。
但很多时候,我们的功能实现又依赖于第三方接口或者外部数据。

例如,我们须要验证用户领取优惠券的几个业务场景:

第一次成功领券超出最大限定次数时领券底层接口非常时领券失落败

而这些场景,我们更好的方案该当是仿照测试数据,也便是利用桩、替人、外部依赖注入等技巧来仿照测试数据,以达到更灵巧、覆盖率更高的测试以及制造所须要的待测试场景,而不是苦苦等待真实小概率事宜的发生。
普通来说,在单元测试时,我们要掌握编排剧情的发展,要风有风,要云有云,而不是看景象出门。

这也是编写单元测试中难度最大、掩护本钱最高的一部分。
为了方便更多同学节制 “造假”技巧,降落对编写单元测试的学习本钱,我根据这几年的履历,从不同的项目情形总结了以下9个造假技巧。

首先,最主要的一个原则是: “给我一个入口,我可以仿照任何数据。
”还有一个条件是:只管即便不修正原来的产品源代码。

其次,常日情形下,部分代码的写法会严重限定、乃至根本无法对其进行仿照,也就无法进行更好地单元测试。
以是不提倡以下写法。

不提倡利用面向过程的函数不提倡利用静态类成员函数不提倡利用private级别的类成员函数/属性技巧1:通过布局参数实现外部依赖注入

很多类在实现功能时,须要拥有与外部其他协作类,即聚合关系,或依赖其他做事实现技能功能,一如委托。
如须要发送邮件时,可能的写法是:

<?phpclass Push { protected $mailService; public function __construct() { $this->mailService = new PHPMailer(); // 其他更多逻辑 ... }}

但这样在测试时,未便利对发送邮件的做事进行仿照(虽然也可以实现子类重载布局方法,但仍须要保持后面其他更多逻辑的同等性,且覆盖率低)。
对此,可以将做事的初始化从内部转向外部,通过布局参数来进行外部依赖注入。

<?phpclass Push { protected $mailService; public function __construct($mailService) { $this->mailService = $mailService; // 其他更多逻辑 ... }}

然后就可以对此邮件做事进行任意的更换和仿照。

$push = new Push(new PHPMailer()); //真实的邮件做事,用于产品代码$push = new Push(new PHPMailer_Mock()); //仿照的邮件做事,用于单元测试技巧2:通过接口参数实现外部依赖注入

这里所说的接口是指类的函数署名所对应的接口声明,而非远程调用的接口做事。

在很多时候,我们的功能类里只是有极个别的操作须要用到极个别的外部资源、协作类或者做事,或者本身便是为了供应做事而无状态。
这样的话,可以直接通过接口参数来实现外部依赖注入。

<?phpclass Push { public function sendEmail($mailService, $title, $content) { // 前期准备 ... $mailService->Send(); // 更多代码 ... }}

这样,在减少一个类成员属性的同时,我们也能更好地进行仿照测试。

$push = new Push();$push->sendEmail(new PHPMailer_Mock(), $title, $content);

顺便提一下,常日比较好的建议是一个类的成员属性数量,不应超过3个。

技巧3:通过提取成员函数制造缝纫点

若依赖的做事只有一个且固定时,利用接口参数注入的办法会显得有点过于重复、繁琐。
这种情形下,可以先将创建资源做事的new操作提取到成员函数,再对此成员函数进行仿照。

如第一步先提取成员函数getMailService():

<?phpclass Push { protected $mailService; public function __construct($mailService) { $this->mailService = $this->getMailService(); // 其他更多逻辑 ... } protected function getMailService() { return new PHPMailer(); } }

第二步在进行单元测试时,可对创建实例的成员函数进行重载更换:

class Push_Mock extends Push { protected function getMailService() { return new PHPMailer_Mock(); }}

末了,利用仿照后的子类进行测试即可。

这种技巧也可用于一些特定难以仿照的操作中,也可以将此真实操作提取到成员函数再进行仿照。

技巧4:通过工厂方法或者资源容器进行外部注入

很多项目也会利用工厂方法或者资源容器的来统一管理、掩护工具实例。

如在系统中利用Factory进行统一管理下,获取用户登录态的代码实现片段:

<?phpclass UserHelper { / 获取用户信息 / public static function getUser() { $saturn = Cookie::get('saturn'); if (! empty($saturn)) { $userModel = Factory::create('UserModel'); $userInfo = $userModel->getMcUser($saturn); return $userInfo; } return NULL; }}

在进行测试时,如果我们只想测试上层业务功能,而难以每次都获取一个真实的用户登录态时,可以对UserModel这一实例进行更换。
但问题在于,工厂方法常日是这样编写的:

<?phpclass Factory { protected static $cache = array(); public static function create($className) { if(isset(self::$cache[$className])) { return self::$cache[$className]; } self::$cache[$className] = new $className(); return self::$cache[$className]; }}

对此,我们须要在启动单元测试之前对这个工厂类进行再改造,即添加一个新的函数Factory ::setCache($className, $obj),再一次 “以假乱真”。

<?phpclass Factory { // 原来的代码 ... public static function setCache($className, $obj) { self::$cache[$className] = $obj; }}

然后,通过“新”工厂方法的绿色通道,就可以在单元测试时通过Mock办法指定任意仿照数据。

<?phpclass PhpUnderControl_EcouponController_Test extends PHPUnit_Framework_TestCase{ public $ecouponController; protected function setUp() { //登录态 $userModelStub = $this->getMock('UserModel'); $userModelStub->expects($this->any()) ->method('getMcUser') ->will($this->returnValue(array( 'tokenId' => '5BA25B3FD90C71AB99B532929F1CF4E1E7F39A50', 'tokenSecret' => '6e215b1e7d0e73f9dc5ba6d4320ee8eb', 'userId' => '1', 'userName' => 'phpunit', ))); // 通过绿色通道指定仿件 Factory::setCache('UserModel', $userModelStub); } protected function tearDown() { // 通过绿色通道注释仿件,规复真实做事 Factory::setCache('UserModel', null); }}技巧5:对利用单例模式的实例进行更换

首先须要把稳到的是,单例模式肯定有其浸染和利用场景,但不应过于泛滥地利用,除非确切知道利用它能带来的好处以及所造成的障碍。

由于单例模式常日结合利用了开头最不提倡的三种写法中的两种写法,即:同时用了静态类函数,又用了private级别的类成员。
这使得在测试制造仿照数据时无法进行。

对此,我们须要前辈行一点 “小手术”,把private的单例成员变量,改成protected级别。

例如曾经在电商平台特卖会中,调用接口中间层的实现类:

<?phpclass App_ShopApi { private static $_instance = null; //实例工具 / 获取单例实例 @return App_ShopApi / public static function getInstance() { if (self::$_instance ===null) { self::$_instance = new App_ShopApi(); } return self::$_instance; } // ……}

此情形下,不得已须要修正产品代码,将private改成protetec。

<?phpclass App_ShopApi { protected static $_instance = null; //实例工具 // ...

在测试前,先利用子类实现更换:

<?phpclass App_ShopApi_Mock extends App_ShopApi { public function setInstance($instance) { parent::$_instance = $instance; }}

再进行更换操作:

<?php// $stub是一个仿件Api_ShopApi_Mock::setInstance($stub);

这样就可以实现更换了。

技巧6:对PHP官方函数进行仿照

PHP官方函数有:exit()、die()、header()、setcookie()等。
而这些如exit和die会直接终止单元测试,而header则会导致警告涌现。
这些都不利于单元测试。

为此,如何既利用官方函数,又能很好进行单元测试呢?答案仍旧是:入口!
在开拓时,我们须要对这些官方原生态的函数进行再封装。
例如在底层时可以利用一个赞助类,类似:

<?php/ 系统函数切入点 /class KernalHelper { public static function headerEx($string, $replace = true, $http_response_code = null) { if ($http_response_code === null) { header($string, $replace); } else { header($string, $replace, $http_response_code); } } public static function exitEx($status = null) { if ($status === null) { exit(); } else { exit($status); } } }

接着,在启动单元测试前,对此Kernal类进行统一更换。

<?phpif (!class_exists('KernalHelper', FALSE)) { class KernalHelper { public static function __callStatic($func, $arguments) { echo "This will call KernalHelper::$func(", implode(',', $arguments) ,") ... \n"; } }}

在仿照时,须要把稳两点。
第一是class_exists()第二参数利用FALSE,避免触发真实类的自动加载。
第二点是可进行打印输出以仿照真实操作,同时方便开拓和调试。

如对付常日接口结果返回处理的相应类:

<?php/ 更符合框架精神的相应类 - 实现IOutput接口,以便可以统一输出处理 - 通用返回格式:code + message + data - 依赖于XssHelper、KernalHelper赞助类,以便更安全、更可测 @author dogstar 20151116 /class ResponseHelper implements IOutput { / -------------------------- 输出 -------------------------- / / 支持JSONP输出返回 / public function display() { // ... ... foreach ($this->headers as $header) { KernalHelper::headerEx($header); } // ... ... }}

运行单元测试后,会看到类似这样的输出:

This will call KernalHelper::headerEx(Cache-Control: no-cache) ... This will call KernalHelper::headerEx(Content-type: application/json; charset=utf-8)技巧7:通过结果网络器对输出进行仿照输出

在利用Yii框架中,我们会对模板视图进行渲染,或者须要返回JSON格式的数据。
而这些都会导致结果直接打印显示出来,虽然PHPUnit支持对输出进行断言、回调。
但是我们希望得到更灵巧的测试办法时,可以对这些输出进行仿照输出。
同时辅以结果网络器,将要输出的结果记录下来,以便校验。

因此,在测试启动文件中,我们须要首先对Controller类进行仿真。

<?phpif (!class_exists('Controller', false)) { class Controller extends CController { public static $view; public static $data; public static $return; public static $code; public static $message; public static $url; public static $terminate; public static $statusCode; // 对返回缺点信息进行结果网络 public function showError( $code, $message='' ){ self::$code = $code; self::$message = $message; } // 对缺点提示进行结果网络 public function showMessage( $data=array(),$code=200 ){ self::$code = $code; self::$data = $data; } // 对模板渲染进行结果网络 public function render($view,$data=null,$return=false) { self::$view = $view; self::$data = $data; self::$return = $return; } // 对重定向进行结果网络 public function redirect($url,$terminate=true,$statusCode=302) { self::$url = $url; self::$terminate = $terminate; self::$statusCode = $statusCode; } }}

这里,可以创造,对付缺点显示、模板渲染、接口输出、重定向等我们都可以进行仿真,并且对当时的场景信息进行保存,以便记录。

然后,对付个中的页面处理,如退出登录:

<?phpclass SiteController extends Controller{ // ... ... / Logs out the current user and redirect to homepage. / public function actionLogout() { Yii::app()->user->logout(); $this->redirect(Yii::app()->homeUrl); }}

接着利用结果网络器进行测试:

<?phpclass PhpUnderControl_SiteController_Test extends PHPUnit_Framework_TestCase { public function testActionLogout() { $rs = $this->siteController->actionLogout(); $this->assertEquals('期望跳转的链接', Controller::$url); }}

每次实行完单元测试后,都应在tearDown()中对Controller::$url、Controller::$data等网络的结果进行擦除,以免对其他测试用例造成滋扰,坚持F.I.R.S.T原则的独立性。

技巧8:仿照第三方接口返回的结果

很多时候,或者说绝大数情形,项目都须要调用第三方接口获取数据。
这使得在开拓过程中、测试过程中都带来了极大不稳定的环境成分。
对此,更好的建议是将接口要求的处理细分为两个环节:接口要求、结果解析。

例如,在接口层中调用大数据排序的接口进行排序的实现。

<?php/ 自动化排序 /class SpecialSort{ protected function _callApi($params) { // 进行接口要求 ... } protected function _parseApiRs($apiRs) { // 解析接口结果 ... }}

然后,在测试时,当我们能指定接口返回的结果时,我们也就能确定终极得到的数据了。
真实的单元测试片段代码如下:

<?phpclass PhpUnderControl_SpecialSort_Test extends PHPUnit_Framework_TestCase{ public $sort; protected function setUp() { parent::setUp(); $this->sort = new SpecialSort_Mock(); } public function testRun() { $floors = array( 1 => array( 'type' => 'data', 'mixedData' => array( array('id' => 11), array('id' => 12), // …… ), ), 2 => array( 'type' => 'data', 'mixedData' => array( array('id' => 21), array('id' => 22), // …… ), ), 3 => array( 'type' => 'html', 'mixedData' => 'haha~', ), ); $this->sort->run($floors); $this->assertNotEmpty($floors); $this->assertEquals(13, $floors[1]['mixedData'][0]['id']); $this->assertEquals(11, $floors[1]['mixedData'][1]['id']); // 更多精确的断言 ... }}

而对付接口结果的返回,则通过下面的代码来仿照实现。

class SpecialSort_Mock extends SpecialSort { protected function _callApi($params) { return array( 'code' => 200, 'data' => array( array('brand_id' => 11, 'is_fixed' => 0, 'sequence' => 2), array('brand_id' => 12, 'is_fixed' => 0, 'sequence' => 3), // …… ), ); }}技巧9:对protected方法进行仿照更换

在一个繁芜的业务处理场景中,我们常日会把不同的子操作提取为protected成员函数。
但过多的路径又难以覆盖到各个成员函数的调用。
或者说,我们既想能仿照到这些protected方法的操作,又能真实对其进行调用。
该怎么办?这时,可通过继续子类添加成员属性的开关办法来进行掌握。

为了大略,随意马虎理解,假设我们有这样的收礼场景:

<?phpclass Gift { public function pickup($uuid, $orderSn) { if ($this->hasBeenPickuped($orderSn)) { return; } // ... ... } protected function hasBeenPickuped($orderSn) { // ... ... }}

为了造一个礼物已领取的假象,我们可以先这样添加一个仿照子类:

<?phpclass Gift_Mock extends Gift { public $isPickUp = null; public function hasBeenPickuped($orderSn) { return $this->isPickUp === null ? parent::hasBeenPickuped($orderSn) : $this->isPickUp; }}

请把稳到两点,一点是我们在仿照子类中添加了成员属性$isPickUp,另一点我们将hasBeenPickuped()方法的访问级别提升到了public。

接着,但可这样同时进行仿照或真实的测试:

<?php$mock = new Gift_Mock();$mock->pickup($uuid, $orderSn); //真实调用$mock->isPickUp = true;$mock->pickup($uuid, $orderSn); //仿照已领取$mock->isPickUp = false;$mock->pickup($uuid, $orderSn); //仿照非领取

综合以上九个造假技巧,可以创造一个原则:测试代码与产品代码分离,且测试时不能改动任何产品代码。
此外,产品代码应只管即便供应一个做事入口,即缝纫点,以便利用桩、替人或仿件。

5.3.4 断言的艺术

PHPUnit官方本身供应了40多个断言,例如常见的有:assertEquals()、assertArrayHasKey()、assertCount()、assertContains()等。
此外,还有对输出、非常、正则等的断言。

其余,在对实际项目进行单元测试时,还要把稳两方面的断言。
一类是对Controller类渲染输出的页面内容的断言,另一类是针对接口做事返回的数据结果进行断言。
如果须要渲染页面,可能利用的是原生PHP的模板办法,也有可能利用的是诸如Smarty、Twig这样的模板引擎。
不管是何种渲染办法或视图模板格式,在对输出页面内容进行断言时,建议不要直接对输出内容断言,也不要直接对页面内容断言,而是改为对供应给视图模板的数据进行断言。

这样做有好处的。
一方面,常日前、后端开拓是分离的,在协作开拓过程中,很有可能后端代码已经实现,如果依赖前端供应的模板文件才能测试,完成断言的话,事情进度就会受到壅塞,达不到高效流转的效果。
另一方面,如果数据与视图领悟在一起,想要还原或提炼数据就非常困难。
虽然可以利用正则或者对输出的内容进行断言,但那样每每都是取巧的做法,会丢失数据类型和精度,不足严谨,同时本钱也很大。
何必那么繁芜呢?直接对供应给视图的数据进行断言即可,如果数据都是精确的,那么接下来只须要关注页面展示是否精确就可以了。

同样地,针对接口做事返回的结果,如果想进行自我验证,在测试时,不是真的要输出终极的返回结果。
而是针对终极将要返回的数据结果本身进行断言即可,由于返回的办法和格式也有可能是多种多样的,比如HTTP+JSON格式,SOAP+工具格式,JSONP格式等。

这里所说的断言艺术,很大略,那便是对最原始的数据进行断言,而非对二次处理输出的结果进行断言。

标签:

相关文章

我国土地利用分类代码的构建与应用

土地利用分类代码是我国土地管理的重要组成部分,是土地资源调查、规划、利用和保护的依据。土地利用分类代码的构建与应用显得尤为重要。本...

SEO优化 2025-02-18 阅读1 评论0

微信跳转微信支付便捷支付体验的秘密武器

移动支付已成为人们日常生活中不可或缺的一部分。作为我国领先的社交平台,微信支付凭借其便捷、安全的支付方式,深受广大用户的喜爱。而微...

SEO优化 2025-02-18 阅读1 评论0

探寻会计科目代码背后的奥秘分类与

会计科目代码是会计信息系统中不可或缺的组成部分,它将企业的经济活动进行分类和归纳,为会计核算、财务分析和决策提供重要依据。本文将从...

SEO优化 2025-02-18 阅读1 评论0