单元测试
单元测试是指检查和验证软件中最小的可测试单元。单元是要测试的最小功能模块。单元测试是软件开发过程中要进行的最低级别的测试活动。软件的独立单元将与程序的其他部分隔离测试。
在PHP中,最小的单元可以引用函数或类。需要验证的是每个函数,每个类的函数都符合我们的期望。
单元测试是什么意思
它可以减少一些细节错误的发生,如错误报告时是否没有错误,输入参数和结果是否符合要求等。
便于今后的改造和维护。在实际工作中,有很多情况需要制作一个版本的函数,但是内部的细节需要在上线后进行调整。如果有一个单元测试,那么更改它会更放心,改进单元测试的过程也是进一步理解需求的过程。
对于平时无法到达的异常分支,更容易找到,并且该分支的处理逻辑可以通过人工测试采取很多步骤来达到,节省了时间
最近,我还尝试为开发中的函数编写单元测试,并意识到了单元测试的好处。在需求中有一个更复杂的时间计算逻辑。一开始,我以为各种情况都是经过深思熟虑的,然后就写了下来。然而,在运行了首先编写的单元测试之后,我仍然发现了几个隐藏的深层次问题我相信我也可以得到它们通过了测试。
问题解决后,在提出测试的过程中需要进行更改。许多关键代码需要更改。通常情况下,由于数据库需要查找各种数据来运行接口,因此很难进行自检,如果数据不能更改,则必须重新运行接口自检。但是,在这次正确地指定了单元测试之后,我们可以根据自己的想法安全大胆地转换代码。经过代码更改、测试运行、代码更改和测试运行的循环,我们很快交付了需求。
单元测试的一些概念
我以前也接触过PHP、python、JS和其他语言,我对这些语言的单元测试有一定的了解。接下来,我将介绍单元测试中的一些常见概念。
断言
为了更详细地理解断言,我推荐一个博客:https://www.jianshu.com/p/9b8c88deed6a
在软件测试中,特别是在单元测试中,一个必要的功能是“断言”。顾名思义,在编写程序时,通常会做出某些假设,即断言用于捕获假设异常。
下面举个例子:
一个简单的函数add有两个参数。它的功能是返回两个参数的和。当我需要验证这个函数的正确性时,我需要模拟两个输入参数,并确定函数的返回值是否是两个输入参数的和。确定返回值是否准确的过程称为断言。
代码语言:javascript复制function add($a, $b)
{
return $a $b;
}
基境
每一个单元测试方法都是一个独立的个体,每次单元测试完毕,需要将数据恢复到正确的状态中,不至于被其他测试方法给影响。
在phpunit中,给出的 TestCase 基类即有两个方法, setUp
和 setDown
分别用于为每个单元测试创建测试对象和清理测试对象
数据供给器
对同一类情况进行测试,通常可以用数据供给器传入不同入参和相应的预期返回值。
测试方法可以接受任意参数。这些参数由数据供给器方法提供。在phpunit中使用 @dataProvider 标注来指定使用哪个数据供给器方法。
php如何集成单元测试
PHP的单元测试依赖一个测试框架:phpunit(官方文档:https://phpunit.readthedocs.io/zh_CN/latest/index.html )
如何安装
可以通过phar的方式安装
$ wget https://phar.phpunit.de/phpunit-7.0.phar $ chmod x phpunit-7.0.phar $ sudo mv phpunit-7.0.phar /usr/local/bin/phpunit $ phpunit --version
也可以通过 composer 进行统一管理
代码语言:javascript复制$ composer require phpunit/phpunit
在 composer.json
中会出现如下依赖
{
"require": {
"phpunit/phpunit": "^7.5"
}
}
并且会出现 vendor/bin/phpunit
文件,直接运行即可
如何编写单元测试
所有类需要继承 PHPUnitFrameworkTestCase
, setUp
函数用于初始化测试对象, setDown
函数用于清理测试对象,更多规范
更具体写法可以查看底部的 举个栗子
phpunit常用断言方法
更多断言方法详见 phpunit 官方文档,基本都能顾名思义。
断言函数 | 作用 | 示例 |
---|---|---|
assertEquals($except, $value) | 断言相等 | $this->assertEquals(2, 1 1) |
assertEmpty($value) | 断言为空 | $this->assertEmpty([]) |
assertNotEmpty($value) | 断言不为空 | $this->assertNotEmpty([1, 2, 3]) |
assertTrue($value) | 断言为真 | $this->assertTrue(1 === 1) |
assertFalse($value) | 断言为假 | $this->assertFalse(1 === ‘1’) |
expectException(Exception $e) | 断言本次测试会出现特定异常 | $this->expectException(Exception::class); throw new Exception(‘测试’, 1002); |
expectExceptionCode($code) | 断言异常状态码 | $this->expectExceptionCode(1002); throw new Exception(‘测试’, 1002); |
expectExceptionMessage($msg) | 断言异常信息 | $this->expectExceptionMessage(‘测试’); throw new Exception(‘测试’, 1002); |
expectOutputString($msg) | 断言输出 | $this->expectOutputString(‘Hello’);echo “Hello”; |
getActualOutput() | 获取实际输出 |
如何运行单元测试
代码语言:javascript复制# 运行全部测试
phpunit
# 运行某个分组的单元测试
phpunit --group GroupA
# 运行指定测试类的所有测试用例
phpunit tests/xxxxTest.php
# 运行所有测试类中满足filter条件的方法
phpunit --filter xxxFunc
# 运行某个测试类中满足filter条件的
phpunit.xml 是什么
phpunit.xml 是一个XML格式的配置文件,能够配置单元测试中的一些默认行为,比如环境变量、启动文件、日志记录等,官方文档如下 https://phpunit.readthedocs.io/zh_CN/latest/configuration.html
一个样例配置如下所示:
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<!--phpunit标签是配置中的核心,这里配置了启动文件 "./tests/bootstrap.php"-->
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="./tests/bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<!--测试套件:非常多的测试用例放在一起即可成为测试套件,执行时会扫描包含的所有 *Test.php文件-->
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<!--这里配置了白名单,只有这里边的代码会被统计覆盖率-->
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app/library</directory>
<directory suffix=".php">./app/models</directory>
</whitelist>
</filter>
<!--这里配置了PHP的环境变量-->
<php>
<server name="APP_ENV" value="product"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="MAIL_DRIVER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
</php>
<!--这里是日志记录,把覆盖率信息保存到 ./tests/codeCoverage-->
<logging>
<log type="coverage-html" target="./tests/codeCoverage"/>
</logging>
</phpunit>
如何查看代码覆盖率
执行 phpunit 之后,根据 <logging>
中的配置,会自动生成代码覆盖率信息至 ./tests/codeCoverage/
,打开其中 index.html
即可查看覆盖率信息。
举个例子:
以一个简单的斐波拉契数列计算函数为例
斐波那契数列由0和1开始,之后的斐波那契系数就是由之前的两数相加而得出。
输入输出分析
根据函数特点,我们可以通过验证已知情况和特殊情况的方式去验证,经过分析结果如下
正常输入的已知情况:
入参 | 预期返回 | 描述 |
---|---|---|
0 | 0 | 规则 |
1 | 1 | 规则 |
2 | 1 | 0 1 = 1 |
3 | 2 | 1 1 = 2 |
4 | 3 | 1 2 = 3 |
5 | 5 | 2 3 = 5 |
6 | 8 | 3 5 = 8 |
… | … | … |
12 | 144 | 55 89 = 144 |
异常输入的情况处理
处理为0,或者抛出异常均可
入参 | 预期返回 | 描述 |
---|---|---|
-1 | 0 | 非正常输入处理为0 |
” | 0 | 非正常输入处理为0 |
1.1 | 0 | 非正常输入处理为0 |
‘文字’ | 0 | 非正常输入处理为0 |
编写测试类
tests/FunctionTest.php
use PHPUnitFrameworkTestCase;
class FunctionsTest extends TestCase
{
/**
* @dataProvider fibon_normal_provider
* @param $input
* @param $except
*/
public function test_fibon_normal($input, $except)
{
$this->assertEquals($except, fibon($input));
}
public function fibon_normal_provider()
{
return [
[0, 0],
[1, 1],
[2, 1],
[3, 2],
[4, 3],
[5, 5],
[6, 8],
[12, 144],
];
}
/**
* @dataProvider fibon_error_provider
* @param $input
* @param $except
*/
public function test_fibon_error($input, $except)
{
$this->assertEquals($except, fibon($input));
}
public function fibon_error_provider()
{
return [
[-1, 0],
[1.1, 0],
['', 0],
['文字', 0],
];
}
}
函数功能实现
(PS:此法效率很差,约莫是 O(n^2) 的复杂度,仅用于此处演示)
functions.php
function fibon($a)
{
if (!is_int($a)) {
return 0;
}
if ($a <= 0) {
return 0;
} elseif ($a == 1) {
return 1;
} else {
return fibon($a - 1) fibon($a - 2);
}
}
运行结果
代码语言:javascript复制vagrant@homestead:~/code/bmtrip/platoReceivable$ phpunit tests/FunctionsTest.php --filter test_fibon
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.
............ 12 / 12 (100%)
Time: 5.77 seconds, Memory: 26.00 MB
OK (12 tests, 12 assertions)
Generating code coverage report in HTML format ... done