phpunit 是一个面向phper的测试框架。于python之pytest,golang之gotest 一样,是可以进行单元测试、代码覆盖率统计等功能的一款工具。phpunit使用手册
PHPUnit 安装
不推荐全局使用安装,phpunit 应该作为某一个项目的依赖进行管理。推荐使用composer来安装,composer的使用不在赘述。
如要全局安装,进行如下操作:
#查看composer的全局bin目录 将其加入系统 path 路径 方便后续直接运行安装的命令
composer global config bin-dir --absolute
#全局安装 phpunit
composer global require --dev phpunit/phpunit
#查看版本
phpunit --version
如要局部安装,只要在项目的 composer.json
文件中添加一行 "phpunit/phpunit": "^6.2"
或者 命令行执行 composer require --dev phpunit/phpunit ^latest
然后执行composer install/update
安装成功后目录项目目录下会有 vendor/bin/phpunit
这个文件,因为我是局部安装,等下执行cmd命令时,就用这个命令文件。
编写 PHPUnit 测试
我的项目目录和编写的代码文件
此处写的是一个校验类 Validate 及测试用例
class Validator {
// 对象本身
private static $objValidate;
// 错误信息
private $_errMsg;
/**
* 这类似于一个校验注册表。
* 校验规则和校验函数、错误信息 映射表。如果不存在校验规则,则不进行校验。
* 扩展新的校验规则方法后,需要在映射表中增加相应项。
*/
private static $_rule = [
'inList' => [
'func' => 'checkInList',
'msg' => '%s is not in the array!',
],
'int' => [
'func' => 'checkInt',
'msg' => '%s is not integer!',
],
'float' => [
'func' => 'checkFloat',
'msg' => '%s is not float!',
],
'string' => [
'func' => 'checkString',
'msg' => '%s is not string!',
],
'bool' => [
'func' => 'checkBool',
'msg' => '%s is not bool!',
],
'array' => [
'func' => 'checkArray',
'msg' => '%s is not array!',
],
'object' => [
'func' => 'checkObject',
'msg' => '%s is not object!',
],
'regex' => [
'func' => 'checkRegex',
'msg' => '%s is not match regex %s!',
],
'max' => [
'func' => 'checkMax',
'msg' => '%s is not lt %d!',
],
'min' => [
'func' => 'checkMin',
'msg' => '%s is not gt %d!',
],
'maxLen' => [
'func' => 'checkMaxLen',
'msg' => '%s length is not lt %d!',
],
'minLen' => [
'func' => 'checkMinLen',
'msg' => '%s length is not gt %d!',
],
'arrayItem' => [
'func' => 'checkArrayItem',
'msg' => '%s array element not match rule!',
],
'func' => [
'func' => null,
'msg' => '%s not match customer function rule!',
],
];
private function __construct()
{
}
/**
* 返回校验对象
* @return Validator
*/
public static function getInstance()
{
if (is_null(static::$objValidate)) {
static::$objValidate = new self();
}
return static::$objValidate;
}
/**
* 错误信息处理
*
* @param string $strField 验证字段
* @param bool $mixRuleValue 验证字段对应值
* @return string
*/
private function _fmtErrMsg($strField, $mixRuleValue = true)
{
if (!$this->_errMsg) {
$this->_errMsg = static::$_rule['func']['msg'];
}
if ($mixRuleValue === true) {
return sprintf($this->_errMsg, $strField);
}
if (is_array($mixRuleValue)) {
$mixRuleValue = json_encode($mixRuleValue);
}
return sprintf($this->_errMsg, $strField, $mixRuleValue);
}
/**
* @param array $rules 校验规则 ['field' => $rule] 的格式 例如:
* [
* 'name' => 'string|maxLen:20|minLen:1|',
* 'age' => 'int|inList:[4,5,6]|func',
* 'sanwei' => ["key"=>"enable","rule"=>"bool"], 支持可选字段的校验,比如$arr['enable']字段如果存在,则必须为bool类型,如果不存在则不需要校验。
* 'sanwei' => ["key"=>"user_id","rule"=>"int|min:0"], 可以递归地校验其中的某个特定元素,满足某个校验规则,比如$arr['user_id']必须是int且大于0。
* 'sanwei' => ["key"=>"$$","rule"=>"int|min:0"], 可以递归地校验其中的每个元素,满足某个校验规则,比如array中的元素都必须是int且大于0。
* ]
* @param array $reqData 需要校验合法性的数
* @param string $errMsg 如果检验不合法,需要返回详细信息
* @param array $option 额外参数,例如 自定义校验函数
* [
* 'age' => $function,
* ]
* @return bool 返回值:如果检验合法,则返回true,如果不合法则返回false
*/
public static function validate(array $arrRules, array $arrData, string &$errMsg, array $arrOption = []): bool
{
// 空规则不校验
if (empty($arrRules)) {
return true;
}
$objValidate = static::getInstance();
// 开始验证
foreach ($arrRules as $strField => $mixRule) {
$arrItemRules = [];
// 存在array元素的验证
if (is_array($mixRule)) {
$arrItemRules['arrayItem'] = $mixRule;
} else {
$arrItemRules = $objValidate->_formatItemRule($mixRule);
}
if ($arrItemRules == false) {
continue;
}
// 逐条规则验证
foreach ($arrItemRules as $strRuleName => $mixRuleValue) {
$mixValue = $arrData[$strField];
// 自定义函数
$funcName = $arrOption[$strField] ?? null;
$bolRet = $objValidate->checkRule($mixValue, $strRuleName, $mixRuleValue, $funcName);
if (!$bolRet) {
$errMsg = $objValidate->_fmtErrMsg($strField, $mixRuleValue);
return false;
}
}
}
return true;
}
/**
* 格式化处理校验规则
*
* @param string $mixRule 校验规则
* @return array|false
*/
private function _formatItemRule($mixRule)
{
if (empty($mixRule)) {
return false;
}
$arrRule = explode('|', $mixRule);
// 过滤掉空项
$arrRule = array_filter($arrRule, function ($item) {
return !empty($item);
});
if (empty($arrRule)) {
return false;
}
// 校验规则整理成数组
$arrItemRules = [];
foreach ($arrRule as $mixItemRule) {
$arrItemRule = explode(':', $mixItemRule);
$arrItemRules[$arrItemRule[0]] = isset($arrItemRule[1]) ? $arrItemRule[1] : true;
}
// 如果有自定义 func,其他规则则不进行校验
if (array_key_exists('func', $arrItemRules)) {
$arrItemRules = [];
$arrItemRules['func'] = true;
}
return $arrItemRules;
}
/**
* 匹配规则
*
* @param mixed $mixValue 要校验的数据
* @param string $strRule 列表中的规则
* @param mixed $mixRuleValue 规则限定值
* @param array $funcName 自定义函数
* @return bool
*/
private function checkRule($mixValue, $strRule, $mixRuleValue, $funcName = null): bool
{
$bolRet = true;
try {
// 运行注册校验的函数
if (array_key_exists($strRule, static::$_rule)) {
// 存在 自定义函数,如果没有可执行函数,则不校验
if ($funcName) {
return $funcName($mixValue, $mixRuleValue);
}
$funcName = static::$_rule[$strRule]['func'] ?? null;
if (!$funcName) {
return true;
}
// 指定方法校验
$bolRet = $this->$funcName($mixValue, $mixRuleValue);
if (!$bolRet) {
$this->_errMsg = static::$_rule[$strRule]['msg'];
}
}
} catch (\Error $e) {
$bolRet = false;
$this->_errMsg = '校验出错:' . $e->getMessage();
}
return $bolRet;
}
/**
* 校验数据的值是否在某个列表内,比如某个数据只能取 'red', 'blue', 'green' 这三个值之一。
* @return bool
*/
public function checkInList($mixValue, $mixRuleList)
{
if (is_string($mixRuleList)) {
$arrRuleList = json_decode($mixRuleList, true);
} else {
$arrRuleList = $mixRuleList;
}
if (is_array($arrRuleList) && in_array($mixValue, $arrRuleList)) {
return true;
}
return false;
}
/**
* 校验数据的类型是否是 integer
* @return bool
*/
public function checkInt($mixValue, $mixRuleValue = null)
{
return is_int($mixValue);
}
/**
* 校验数据的类型是否是 float
* @return bool
*/
public function checkFloat($mixValue, $mixRuleValue = null)
{
return is_float($mixValue);
}
/**
* 对于array类型,按校验规则校验
* @return bool
*/
public function checkArrayItem($arrValue, $arrRule)
{
if (!is_array($arrValue)) {
return false;
}
// 规则
$strKey = $arrRule['key'];
$arrItemRules = $this->_formatItemRule($arrRule['rule']);
if ($arrItemRules == false) {
return true;
}
return $this->checkItem($strKey, $arrValue, $arrItemRules);
}
/**
* 对于array类型,递归进行校验
*
* @param string $strTargetKey 要校验的目标键
* @param array $arrValue 要校验的数据值
* @param array $arrItemRules 校验规则
* @return bool
*/
private function checkItem($strTargetKey, $arrValue, $arrItemRules)
{
foreach ($arrValue as $strKey => $mixValue) {
if (is_array($mixValue)) {
return $this->checkItem($strTargetKey, $mixValue, $arrItemRules);
}
if ($strKey === $strTargetKey || $strTargetKey == '$$') {
// 逐条规则验证
foreach ($arrItemRules as $strRuleName => $strRuleValue) {
$bolRet = $this->checkRule($mixValue, $strRuleName, $strRuleValue);
if ($bolRet == false) {
return false;
}
}
}
}
return true;
}
}
相应的测试文件 test/ValidateTest.php 只写两个方法(只是示意)。PHPUnit\Framework\TestCase
,命名类名+Test, 每个需要测试的方法命名都是 test+对应业务方法,如下所示。
use PHPUnit\Framework\TestCase;
class ValidateTest extends TestCase {
/**
* 测试Validator::validate()
*/
public function testValidate()
{
$strErrMsg = '';
// 校验规则
$arrSchema = [
'age' => 'int|max:99|min:1',
'name' => 'string|require|maxLen:12|mixLen:1',
'color' => 'inList:["blue","red","green"]',
'word' => 'regex:\w+',
'sanwei' => [
"key" => "enable",
"rule" => "bool",
],
];
// 测试输入数据
$reqData = [
'name' => 'yyb',
'age' => 89,
'color' => 'blue',
'word' => 'hsoefheo',
'sanwei' => [
'enable' => true,
'name' => 'xxx',
],
];
// 自定义函数
$function = function ($value) {
if ($value == 13) {
return true;
}
return false;
};
$option = [
'age' => $function,
];
$mixRet = Validator::validate($arrSchema, $reqData, $strErrMsg, $option);
$this->assertFalse($mixRet);
// 无自定义函数
$mixRet = Validator::validate($arrSchema, $reqData, $strErrMsg);
$this->assertTrue($mixRet);
}
/**
* 测试 checkInList
*/
public function testCheckInList()
{
// 不在目标数组中
$strTarget = 'yellow';
$arrList = ['red', 'blue', 'green'];
$obj = Validator::getInstance();
$bolret = $obj->checkInList($strTarget, $arrList);
$this->assertFalse($bolret);
// 成功
$strTarget = 'blue';
$arrList = ['red', 'blue', 'green'];
$bolret = $obj->checkInList($strTarget, $arrList);
$this->assertTrue($bolret);
}
}
执行单元测试
在项目目录下,命令行执行:./vendor/bin/phpunit test/ValidateTest.php
MacintoshdeMacBook-Pro phpdir % ./vendor/bin/phpunit test/ValidateTest.php
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.
.............. 14 / 14 (100%)
Time: 58 ms, Memory: 4.00MB
OK (14 tests, 56 assertions)
共14个单测,每一个方法有一个测试用例,共56个断言。
代码覆盖率
- phpunit的代码覆盖率功能利用了
Xdebug
,在正确运行前需要安装xdebug,我用的是mac 电脑,mac电脑本身自带php 和xdebug扩展,在配置文件中启用配置即可。查看mac目录下文件夹ls /usr/lib/php/extensions
,cd 对应的目录cd no-debug-non-zts-20180731
MacintoshdeMacBook-Pro no-debug-non-zts-20180731 % pwd
/usr/lib/php/extensions/no-debug-non-zts-20180731
MacintoshdeMacBook-Pro no-debug-non-zts-20180731 % ls
opcache.so xdebug.so
然后配置php.ini 。vim /etc/php.ini
,如果没有php.ini,先执行 cp /etc/php.ini.default /etc/php.ini
,最后添加如下:
[xdebug]
zend_extension=/usr/lib/php/extensions/no-debug-non-zts-20180731/xdebug.so
xdebug.remote_enable = 1
xdebug.remote_connect_back=1
xdebug.remote_port = 9123
xdebug.scream=0
xdebug.show_local_vars=1
xdebug.idekey=PHPSTORM
xdebug.remote_enable=On
xdebug.remote_autostart=On
执行 php -m 查看是否安装成功
- 到这里终于可以执行代码覆盖率的测试了,其实没有必要追求100%的代码覆盖率,即使代码覆盖率达到了100%,也不代表测试质量就是很高,不代表涵盖了所有的情况。代码覆盖率只是一个参考值。
可以生成的格式
--coverage-clover <file> Generate code coverage report in Clover XML format.
--coverage-crap4j <file> Generate code coverage report in Crap4J XML format.
--coverage-html <dir> Generate code coverage report in HTML format.
--coverage-php <file> Export PHP_CodeCoverage object to file.
--coverage-text=<file> Generate code coverage report in text format.
--coverage-xml <dir> Generate code coverage report in PHPUnit XML format.
执行命令
./vendor/bin/phpunit \
--bootstrap vendor/autoload.php \
--coverage-html=reports/ \
--whitelist src/ \
test/ValidateTest.php
其中 --whitelist dir
来设定需要覆盖率的业务代码路径。
#查看覆盖率报告
cd reports/ && php -S 0.0.0.0:8899
以上只是比较简单的使用phpunit ,具体更高级的用法还有很多,大家可以参照phpunit中文手册和文章开头的 phpunit使用手册。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!