PHPUnit 单元测试使用

699 阅读3分钟

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"

485ff188c002ce952ee44546a.png 或者 命令行执行 composer require --dev phpunit/phpunit ^latest 然后执行composer install/update 安装成功后目录项目目录下会有 vendor/bin/phpunit 这个文件,因为我是局部安装,等下执行cmd命令时,就用这个命令文件。

编写 PHPUnit 测试

我的项目目录和编写的代码文件

72739.png 此处写的是一个校验类 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

3407c.png

以上只是比较简单的使用phpunit ,具体更高级的用法还有很多,大家可以参照phpunit中文手册和文章开头的 phpunit使用手册