最近开始一个新的项目,涉及到了网络通信的客户/端服务端,刚开始都是边写边调试,完成之后觉得有必要留下相关的测试示例,以便日后使用调试。一些PHP项目会把示例写在文件开头的注释里面,但这样又没办法验证是否能正确运行。
workerman是在文件最后留下的运行代码,如RpcClient.php
// ==以下调用示例== if(PHP_SAPI == 'cli' && isset($argv[0]) && $argv[0] == basename(__FILE__)) { //这里是RpcClient的测试用例 }
PHPUnit则是这样的
#!/usr/bin/env php <?php if (__FILE__ == realpath($GLOBALS['_SERVER']['SCRIPT_NAME'])) { $phar = realpath($GLOBALS['_SERVER']['SCRIPT_NAME']); $execute = true; } else { $files = get_included_files(); $phar = $files[0]; $execute = false; }
这有点像python
if __name__ == "__main__": //这里运行相关调用
这样就可以对单个文件的代码边看边测试,但是这会在相关的代码里面混入不必要的测试用例代码,当有依赖于外部文件又不是很方便,于是便想把这部分测例代码独立出来。
PHPUnit是xUnit测试家族的一员,可以方便对测试代码进行管理,还可以结合其他扩展。依照官网步骤开始:
wget https://phar.phpunit.de/phpunit.phar chmod +x phpunit.phar sudo mv phpunit.phar /usr/local/bin/phpunit phpunit --version #PHPUnit 4.7.7 by Sebastian Bergmann and contributors.
写一个测试代码(PHPUnit也可以自动生成对应的测试代码)
<?php namespace Test\Client; use Client\RPC; class RPCTest extends \PHPUnit_Framework_TestCase{ public $pClient = null; public $nMatchID = 567; public function __construct(){ parent::__construct(); $arrAddress = array( 'tcp://127.0.0.1:9001' ); // 配置服务端列表 RPC::config($arrAddress); $this->nMatchID = 567; $this->pClient = RPC::instance('\Prize\Service'); } public function testGet(){ $mxRes = $this->pClient->get($this->nMatchID); $this->assertTrue(is_array($mxRes)); $this->assertGreaterThan(0, count($mxRes)); } public function testSendGet(){ $this->assertTrue($this->pClient->send_get($this->nMatchID)); } /** * 使用依赖,以便保证在同一个socket链接之内 * @depends testSendGet */ public function testRecvGet(){ $mxRes = $this->pClient->recv_get($this->nMatchID); $this->assertTrue(is_array($mxRes)); $this->assertGreaterThan(0, count($mxRes)); } }
这边的使用了外部类RPC,于是在Bootstrap.php里面定义相关的自动加载规则,以便测试时自动加载
<?php define('STIE_PATH', getcwd().'/'); define('ROOT_PATH', STIE_PATH); define('LIB_PATH', ROOT_PATH.'Lib/'); define('TEST_PATH', ROOT_PATH.'Test/'); spl_autoload_register(function($p_strClassName){ $arrClassName = explode('\\', trim($p_strClassName, '\\')); if(!empty($arrClassName)){ if($arrClassName[0] == 'Test'){ $strPath = ROOT_PATH.implode(DIRECTORY_SEPARATOR, $arrClassName).'.php'; } else{ $strPath = LIB_PATH.implode(DIRECTORY_SEPARATOR, $arrClassName).'.php'; } } if(is_file($strPath)){ include $strPath; } else{ throw new Exception("File not find : ".$strPath); } });
现在运行测试代码
phpunit --bootstrap Test/Bootstrap.php Test/
出现了一些警报和错误。其中一个提示找不到PHPUnit_Extensions_Story_TestCase文件:
PHP Warning: include(/usr/share/nginx/html/tutorial/mars/Lib/PHPUnit_Extensions_Story_TestCase.php): failed to open stream: No such file or directory in /usr/share/nginx/html/tutorial/mars/Test/Bootstrap.php on line 20
这文件是一个PHPUnit的基于故事的行为驱动开发(BDD)扩展,于是上网找了下想安装。
PHPUnit现在只支持Composer安装,如果用PEAR安装会提示失败
sudo pear channel-discover pear.phpunit.de Discovering channel pear.phpunit.de over http:// failed with message: channel-add: Cannot open "http://pear.phpunit.de/channel.xml" (File http://pear.phpunit.de:80/channel.xml not valid (received: HTTP/1.1 410 Gone )) Trying to discover channel pear.phpunit.de over https:// instead Discovery of channel "pear.phpunit.de" failed (channel-add: Cannot open "https://pear.phpunit.de/channel.xml" (File https://pear.phpunit.de:443/channel.xml not valid (received: HTTP/1.1 410 Gone )))
先安装Composer
curl -sS https://getcomposer.org/installer | php mv composer.phar /usr/local/bin/composer composer -V
创建一个composer.json,以便自动下载相关的库
{ "require-dev": { "phpunit/phpunit": "4.7.*", "phpunit/phpunit-selenium": ">=1.4", "phpunit/dbunit": ">=1.3", "phpunit/phpunit-story": "*", "phpunit/php-invoker": "*" }, "autoload": { "psr-0": {"": "src"} }, "config": { "bin-dir": "bin/" } }
运行composer,注意:composer在国内慢的要死,可以参照这里。
composer install --dev
便会在同级目录下面生成一个vendor目录,各自的依赖库都下载在里面,并且生成了autolaod.php,用于加载相关的类库。编辑Bootstarp.php增加一行
include ROOT_PATH."vendor/autoload.php";
再次运行PHPUnit
[vagrant@vagrant-centos64 mars]$ phpunit --bootstrap Test/Bootstrap.php Test/KM/Client/RPCTest PHPUnit 4.7.7 by Sebastian Bergmann and contributors. EES Time: 540 ms, Memory: 14.25Mb There were 2 errors: 1) Test\KM\Client\RPCTest::testGet stream_socket_client(): unable to connect to tcp://127.0.0.1:9001 (Connection refused) /usr/share/nginx/html/tutorial/mars/Lib/Client/Stream.php:19 /usr/share/nginx/html/tutorial/mars/Lib/Client/Stream.php:16 /usr/share/nginx/html/tutorial/mars/Lib/Client/RPC.php:174 /usr/share/nginx/html/tutorial/mars/Lib/Client/RPC.php:141 /usr/share/nginx/html/tutorial/mars/Lib/Client/RPC.php:130 /usr/share/nginx/html/tutorial/mars/Test/Client/RPCTest.php:21 /usr/share/nginx/html/tutorial/mars/Test/Client/RPCTest.php:21 2) Test\KM\Client\RPCTest::testSendGet stream_socket_client(): unable to connect to tcp://127.0.0.1:9001 (Connection refused) /usr/share/nginx/html/tutorial/mars/Lib/Client/Stream.php:19 /usr/share/nginx/html/tutorial/mars/Lib/Client/Stream.php:16 /usr/share/nginx/html/tutorial/mars/Lib/Client/RPC.php:174 /usr/share/nginx/html/tutorial/mars/Lib/Client/RPC.php:141 /usr/share/nginx/html/tutorial/mars/Lib/Client/RPC.php:116 /usr/share/nginx/html/tutorial/mars/Test/Client/RPCTest.php:26 /usr/share/nginx/html/tutorial/mars/Test/Client/RPCTest.php:26 FAILURES! Tests: 2, Assertions: 0, Errors: 2, Skipped: 1.
这下子没有警报了,只有详细的测试错误报告。刚才的测试代码里面testRecvGet方法依赖于testSendGet,一旦后者失败了,前者便会被跳过。
启动服务端再次运行测试代码
[vagrant@vagrant-centos64 mars]$ phpunit --colors --bootstrap Test/Bootstrap.php Test/ PHPUnit 4.7.7 by Sebastian Bergmann and contributors. ... Time: 496 ms, Memory: 14.25Mb OK (3 tests, 5 assertions)
这下子全部测试通过了。
前面运行测试是这样的:phpunit –bootstrap Test/Bootstrap.php Test/。–bootstrap是指在运行测试时首先加载的文件可以在这里面定义一些配置,自动加载规则,最后一个参数Test/是指要测试的目录,也可以指明运行某个测试用例,比如Test/Client/RPCTest,指运行RPCTest这个测试用例。
PHPUnit还有许多其他的命令行参数选项,如果每次这么输入也挺麻烦的,可以做成配置文件,加载运行就好了,比如phpunit.xml.dist
<?xml version="1.0" encoding="UTF-8"?> <phpunit backupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" syntaxCheck="false" bootstrap="Test/Bootstrap.php" > <testsuites> <testsuite name="Lib Test Suite"> <directory>./Test/</directory> </testsuite> </testsuites> </phpunit>
然后运行:phpunit -c phpunit.xml.dist 就可以了,更多XML配置参考这里。还可以在这基础上更改执行配置,比如phpunit -c phpunit.xml.dist Test/Client/RPCTest,便是依照配置仅运行RPCTest。
以前挺纠结要不要写测试代码,一方面有测试代码的话,每次变更可以自动检测是否通过,另一方面,详细的测试用例需要大量时间去考虑,并且多数情况下都是服务模块之间的互相调用,难以预估结果,超出单元测试范围。现在看来写的这部门测试代码除了验证程序是否准确外,还可以作为示例文档。
参考链接:
PHPUnit: Failed opening required `PHPUnit_Extensions_Story_TestCase.php`
Issues while installing pear.phpunit.de/PHPUnit
单元测试
“单元测试要做多细?”
我讨厌单元测试:滕振宇谈如何进行单元测试
自动化单元测试实践之路
Unit Testing Tutorial Part I: Introduction to PHPUnit