最近开始一个新的项目,涉及到了网络通信的客户/端服务端,刚开始都是边写边调试,完成之后觉得有必要留下相关的测试示例,以便日后使用调试。一些PHP项目会把示例写在文件开头的注释里面,但这样又没办法验证是否能正确运行。
workerman是在文件最后留下的运行代码,如RpcClient.php
1 2 3 4 5 | // ==以下调用示例== if (PHP_SAPI == 'cli' && isset( $argv [0]) && $argv [0] == basename ( __FILE__ )) { //这里是RpcClient的测试用例 } |
PHPUnit则是这样的
1 2 3 4 5 6 7 8 9 10 | #!/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
1 2 | if __name__ = = "__main__" : / / 这里运行相关调用 |
这样就可以对单个文件的代码边看边测试,但是这会在相关的代码里面混入不必要的测试用例代码,当有依赖于外部文件又不是很方便,于是便想把这部分测例代码独立出来。
PHPUnit是xUnit测试家族的一员,可以方便对测试代码进行管理,还可以结合其他扩展。依照官网步骤开始:
1 2 3 4 5 | 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也可以自动生成对应的测试代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?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里面定义相关的自动加载规则,以便测试时自动加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?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 ); } }); |
现在运行测试代码
1 | phpunit --bootstrap Test /Bootstrap .php Test/ |
出现了一些警报和错误。其中一个提示找不到PHPUnit_Extensions_Story_TestCase文件:
1 | 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安装会提示失败
1 2 3 4 5 6 | 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
1 2 3 | curl -sS https: //getcomposer .org /installer | php mv composer.phar /usr/local/bin/composer composer -V |
创建一个composer.json,以便自动下载相关的库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | { "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在国内慢的要死,可以参照这里。
1 | composer install --dev |
便会在同级目录下面生成一个vendor目录,各自的依赖库都下载在里面,并且生成了autolaod.php,用于加载相关的类库。编辑Bootstarp.php增加一行
1 | include ROOT_PATH. "vendor/autoload.php" ; |
再次运行PHPUnit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | [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,一旦后者失败了,前者便会被跳过。
启动服务端再次运行测试代码
1 2 3 4 5 6 7 8 | [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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <? 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
Pingback引用通告: 使用Phar打包发布PHP程序 | 勇气
Pingback引用通告: PagerDuty Integration Development | 勇气