如何使用PHPUnit进行PHP单元测试:测试覆盖率与编写运行指南

[id_2107796060]
单元测试被称作模块测试,它是针对程序模块进行正确性检验的测试工作,而程序单元是应用的最小可测试部件。在过程化编程中,单个程序、函数、过程等是一个单元;在面向对象编程中,最小单元是方法,包括基类(超类)、抽象类或派生类(子类)中的方法。[id_349021690]
工程
创建一个测试工程,我们使用 composer 来充当phpunit 的启动器,不再运用官方的phpunit.phar 文件了。
[id_888790226]# composer init
更改生成的composer.json文件,添加以下依赖库。
...
[id_[id_1620407127]93563121]: {
"[id_1320200828]": ">=7.2"[id_963831753]
"phpunit/phpunit": "^7",
"friendsofphp/php-cs-fixer": "^2.3",
"squizlabs/php_codesniffer": "^3.0",
"[id_234057295]": "^2.0"
[id_1551601130]
"autoload": {
"psr-4": [id_1701136654]
"Test\\": "tests/src"
}
}
...
安装依赖
[meshell@phpunit]# composer install
测试断言
phpunit 具备 50 个断言函数。这些断言函数基本将所有的断言测试都涵盖了。同时,它还提供了数十个注解功能。接下来让我们一起学习断言方法的使用吧。
namespace Test;
use PHPUnit\Framework\TestCase;
/**
[id_681006363]
* @package Test`
[id_111817891]
class AssertTest extends TestCase
{
/**
*
*/
public function testDataType()
{
$this->assertIsInt(11); // 测试变量不是整型
$this->assertIsArray([]); // 测试变量不是数组
$this->assertIsFloat(1.1); // 测试变量不是符点型
$this->assertIsBool(false); // 测试变量不是bool类型
$this->assertIsCallable(function() {}); // 测试变量不是闭包
$this->assertIsIterable(new \stdClass()); // 测试变量不是迭代器
}
/**
*
*/
public function testEqual()
{
$base = 111;
$this->assertEquals($base, 2222); // 测试相等性
}
public function testContains()
{
$haystack = "test string contains";
$this->assertContains("test", $haystack); // 测试字符包含关系
}
/**
*
*/
public function testArray()
{
$hash = [
'body' => "hello world"
];
$key = 'body';
$this->assertArrayHasKey($key, $hash); // 测试key是否存在
}
}
建立测试单元时,测试类需继承 TestCase,测试方法应以 test 开头,例如 testDataType 等方法。仅需在测试方法中调用想要测试的断言,如 assertIsArray 等。可查看全部的断言方法。
执行测试
通过命令来执行测试用例,你能够借助 IDE 或者 phpunit.xml 配置文件迅速执行测试。我们会运用以下命令来执行上述用例。
[meshell@phpunit]使用 php 运行 vendor/phpunit/phpunit/phpunit 命令,且不使用配置文件,对 tests/src/AssertTest.php 进行测试
上述命令会对测试用例中的所有方法进行测试,正常情况下你将会看到如下信息。
PHPUnit 7.5.11 by Sebastian Bergmann and contributors.
FF. 整体情况是 4 份占 4 份,占比为 100%。
Time: 31 ms, Memory: 4.00 MB
There were 2 failures:
1) Test\AssertTest::testDataType
Failed asserting that一个 stdClass Object 对象,其对象标识为 000000001694dcb0000000001f4ce956 。is of type "iterable".
在 /Users/meshell/Projects/php/loocode-example/phpunit/tests/src 目录下的 AssertTest.php 文件的第 23 行。
2) Test\AssertTest::testEqual
Failed asserting that 2222 matches expected 111.
在 /Users/meshell/Projects/php/loocode-example/phpunit/tests/src 目录下的 AssertTest.php 文件的第 32 行。
FAILURES!
结果会告知你测试方法的数量、断言的数量以及断言失败的数量。例如,像上面的结果那样,对应的是 4 个测试,9 个断言,2 个失败。
如果你想测试单个方法可以使用下面的命令
# testDataType 需要测试的方法
# Test\AssertTest 需要测试的类
[meshell@phpunit]使用 php 运行 vendor/phpunit/phpunit/phpunit 命令,不使用配置文件,通过筛选条件“/(::testDataType)(.*)?$/”来测试 Test\AssertTest 类中的 tests/src/AssertTest.php 文件
查看phpunit参数
测试依赖
通过测试依赖,能够把一个测试方法的结果传递给另一个测试方法。我们无需编写任何代码,只需为主方法加上一个注解名即可实现这项功能。
namespace Test;
use PHPUnit\Framework\TestCase;
class DependenciesTest extends TestCase
{
public function testEmpty()
{
$stack = [];
$this->assertEmpty($stack);
return $stack;
}
/**
* @depends testEmpty
*/
public function testPush(array $stack)
{
array_push($stack, 'foo');
$this->assertEquals('foo', $stack[count($stack)-1]);
$this->assertNotEmpty($stack);
return $stack;
}
/**
* @depends testPush
*/
public function testPop(array $stack)
{
$this->assertEquals('foo', array_pop($stack));
$this->assertEmpty($stack);
}
}
我们给需要依赖的测试方法添加了@depends testPush 注解。其中,testPush 是被依赖的方法名,testEmpty 也是被依赖的方法名。
[meshell@phpunit]使用 php 运行 vendor/phpunit/phpunit/phpunit ,且不使用配置文件,对 tests/src/DependenciesTest.php 进行测试
PHPUnit 7.5.11
... 3 / 3 (100%)
Time: 72 ms, Memory: 4.00 MB
OK (3 tests, 5 assertions)
数据供给器
测试方法能够接受各种参数。这些参数是由数据供给器方法 additionProvider()予以提供的。通过使用@dataProvider 标注来明确指定要使用的是哪个数据供给器方法。
数据供给器方法需声明为 public。其返回值有两种情况,一是一个数组,且该数组的每个元素也是数组;二是一个实现了 Iterator 接口的对象,对其进行迭代时每步会产生一个数组。每个数组都是测试数据集的一部分,会以其内容作为参数来调用测试方法。
namespace Test;
use PHPUnit\Framework\TestCase;
/**
* Class ProviderTest
* @package Test
*/
class ProviderTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function testAdd($a, $b, $expected)
{
$this->assertEquals($expected, $a + $b);
}
/**
* @return array
*/
public function additionProvider()
{
return [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 3]
];
}
}
[meshell@phpunit]使用 php 运行 vendor/phpunit/phpunit/phpunit 命令,且不使用配置文件,对 tests/src/ProviderTest.php 进行测试
PHPUnit 7.5.11 by Sebastian Bergmann and contributors.
...F 4 / 4 (100%)
Time: 28 ms, Memory: 4.00 MB
There was 1 failure:
1Test\ProviderTest 进行 testAdd 操作并带有数据set #3 (1, 1, 3)
Failed asserting that 2 matches expected 3.
在 /Users/meshell/Projects/php/loocode-example/phpunit/tests/src 路径下的 ProviderTest.php 文件中。21
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
如果测试从@dataProvider 方法和一个或多个@depends 测试接收数据,那么数据供给器的参数先于所依赖测试的参数。所依赖测试的参数对于每个数据集是一样的。
namespace Test;
use PHPUnit\Framework\TestCase;
/**
* Class ProviderTest
* @package Test
*/
class ProviderTest extends TestCase
{
...
public function provider()
{
return [['provider1'], ['provider2']];
}
public function testProducerFirst()
{
$this->assertTrue(true);
return 'first';
}
public function testProducerSecond()
{
$this->assertTrue(true);
return 'second';
}
/**
* @depends testProducerFirst
* @depends testProducerSecond
* @dataProvider provider
*/
public function [id_1395004207]()
{
$this->assertEquals(
['provider1', 'first', 'second'],
func_get_args()
);
}
}
[meshell@phpunit]# php vendor/phpunit/phpunit/phpunit --no-configuration tests/src/ProviderTest.php
PHPUnit 7.5.11 by Sebastian Bergmann and contributors.
...F...F 8 / 8 (100%)
Time: 51 ms, Memory: 4.00 MB
There were 2 failures:
1) Test\ProviderTest::testAdd with data set #3 (1, 1, 3)
Failed asserting that 2 matches expected 3.
/Users/meshell/Projects/php/loocode-example/phpunit/tests/src/ProviderTest.php:21
2Test\ProviderTest 进行了对 testConsumer 的测试,并且带有数据set #1 ('provider2')
断言两个数组相等的操作失败了。
--- Expected
+++ Actual
@@ @@
Array (
- 0 => 'provider1'
+ 0 => 'provider2'
1 => 'first'
2 => 'second'
)
/Users/meshell/Projects/php/loocode-example/phpunit/tests/src/ProviderTest.php:63
FAILURES!
Tests: 8, Assertions: 8, Failures: 2.
注意:若一个测试依赖于另一个使用了数据供给器的测试,那么只有当被依赖的测试至少能在一组数据上取得成功时,依赖于它的测试才会运行。并且,使用了数据供给器的测试,其运行结果无法注入到依赖于此测试的其他测试当中。
所有数据供给器方法的执行是在调用 setUpBeforeClass 静态方法以及首次调用 setUp 方法之前完成的。所以,不能在数据供给器中使用在这两个方法内创建的变量。这是必须的,因为只有这样,PHPUnit 才能计算出测试的总数量。
异常测试
可以使用 expectException 方法来进行代码的异常测试,也可以使用 @expectedException 注解来进行代码的异常测试。
namespace Test;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
class ExceptionTest extends TestCase
{
public function testException()
{
$this期望抛出 InvalidArgumentException 异常。class);
}
/**
* @expectedException InvalidArgumentException
*/
public function testAException()
{
throw new InvalidArgumentException("无效的参数", 500);
}
}
[meshell@phpunit]使用 php 运行 vendor/phpunit/phpunit/phpunit 命令,且不使用配置文件,对 tests/src/ExceptionTest.php 进行测试。
PHPUnit 7.5.11 by Sebastian Bergmann and contributors.
F. 2 / 2 (100%)
Time: 28 ms, Memory: 4.00 MB
There was 1 failure:
1Test 的 ExceptionTest 类进行了 testException 测试。
断言失败,即出现了特定类型的异常。type "InvalidArgumentException" is thrown.
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
可以看执行之后的结果,第二个方法抛出了异常被断言捕获。除了 expectException 之外,还有测试 code 和 message 的方法及注解,分别是 expectExceptionCode、expectExceptionMessage、@expectedExceptionCode、@expectedExceptionMessage。官方后期会移除对 expectException 和 @expectedException 的支持,需要使用明确的 code 和 message 进行测试。
错误测试
在默认的情况下,PHPUnit 会把测试在执行过程中触发的 PHP 错误、警告、通知都转变为异常。凭借这些异常,是能够进行测试的。例如,可以预期测试会触发 PHP 错误,就像加载了一个不存在的文件那样。
namespace Test;
use PHPUnit\Framework\Error\Error;
use PHPUnit\Framework\TestCase;
class ErrorTest extends TestCase
{
/**
* @expectedException Error
*/
public function testNotFile()
{
include "not.file.php";
}
}
[meshell@phpunit]使用 php 运行 vendor/phpunit/phpunit/phpunit ,且不使用配置文件,对 tests/src/ErrorTest.php 进行测试
PHPUnit 7.5.11 by Sebastian Bergmann and contributors.
E 1 / 1 (100%)
Time: 27 ms, Memory: 4.00 MB
There was 1 error:
1) Test\ErrorTest::testNotFile
include(not.file.php) 无法打开流:没有这样的文件或目录。
在 /Users/meshell/Projects/php/loocode-example/phpunit/tests/src 目录下的 ErrorTest.php 文件中:17
/Users/meshell/Projects/php/loocode-example/phpunit/tests/src/ErrorTest.php:17
ERRORS!
Tests: 1, Assertions: 0, Errors: 1.
输出测试
有时候,要断言某方法在运行过程中生成了预期的输出,比如通过 echo 或 print 来实现。PHPUnit\Framework\TestCase 类利用 PHP 的输出缓冲特性,为这一需求提供了必要的功能支持。可以通过 expectOutputString()方法来设定预期的输出。如果没有产生预期的输出,测试将计为失败。
namespace Test;
use PHPUnit\Framework\TestCase;
class PrintEchoTest extends TestCase
{
public function testPrint()
{
$this->expectOutputString('foo');
print 'foo';
}
public function testEcho()
{
$this->expectOutputString('hell');
echo "hello";
}
}
[meshell@phpunit]使用 php 运行 vendor/phpunit/phpunit/phpunit 命令,且不使用配置文件,对 tests/src/PrintEchoTest.php 进行测试。
PHPUnit 7.5.11 by Sebastian Bergmann and contributors.
.F 2 / 2 (100%)
Time: 26 ms, Memory: 4.00 MB
There was 1 failure:
1Test\PrintEchoTest 进行 testEcho 测试。
断言两个字符串相等的操作失败了。
--- Expected
+++ Actual
@@ @@
-'hell'
+'hello'
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
用XML配置测试套件
定义一个用于当前测试的 xml 配置文件。比如像下面这样的配置示例,它会在当前的 tests 目录中去查找所有以 Test.php 结尾的文件来进行测试。你能够查看以获取更详细的配置说明。
<phpunit
backupGlobals="true"
colors="true"
stopOnFailure="false"
bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="App test">
<directory suffix="Test.php">testsdirectory>
testsuite>
testsuites>
phpunit>
执行下面命令便可测试全部文件。若--configuration 未跟文件名,它就会默认以phpunit.xml 或 phpunit.xml.dist 作为文件来加载。
[meshell@phpunit]使用 php 运行 vendor/phpunit/phpunit/phpunit ,并配置为 phpunit.xml.dist 。
代码覆盖率
代码覆盖率是一种指标,用于衡量特定测试套件对程序源代码的测试程度。高代码覆盖率的程序比低代码覆盖率的程序测试得更彻底,包含软件 bug 的可能性也更低。
在有测试覆盖率之前,你得先安装 PHP_CodeCoverage 以及 Xdebug 扩展。在进行测试之前,我们要编写一些需要测试覆盖率的代码。
namespace App;
class Coverage
{
public function add()
{
return 1 + 1;
}
public function sub()
{
return 2 - 1;
}
public function div()
{
return 4 / 2;
}
public function mul()
{
return 2 * 2;
}
public function find(array $sort, int $find)
{
return $this->binarySearch($sort, $find);
}
public function binarySearch(array $sort, $find)
{
$low = 0;
$high = count($sort) - 1;
while ($low <= $high) {
$mid = (int) floor(($low + $high) / 2);
if($sort[$mid] == $find) {
return true;
}
if ($find < $sort[$mid]) {
$high = $mid -1;
} else {
$low = $mid + 1;
}
}
return false;
}
}
上面就是我们需要测试的代码,我们再为它增加一个测试用例.
namespace Test;
use App\Coverage;
use PHPUnit\Framework\TestCase;
class CoverageTest extends TestCase
{
/**
* @var null | Coverage
*/
public $coverage = null;
public function setUp()
{
parent::setUp(); 以下是改写后的句子:用中文将横线下的句子进行改写,要避免使用原句用过的词,可对句子结构和用词进行调整与充实,以达到最大差异且保留原意,同时严禁修改专有名词和穿插英文单词。
$this->coverage = new Coverage();
}
public function testCoverage()
{
$this->assertIsInt($this->coverage->add());
$this->assertIsInt($this->coverage->div());
$this->assertIsInt($this->coverage->mul());
$this->assertIsInt($this->coverage->sub());
$array = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
];
$find = 2;
$this->assertIsBool($this->coverage->find($array, $find));
}
}
[meshell@phpunit]使用 php 并设置 -dxdebug.coverage_enable=1 ,然后执行 vendor/phpunit/phpunit/phpunit ,同时指定 --coverage-clover CoverageTest.xml 和 --configuration phpunit.xml.dist ,最后对 Test\CoverageTest 和 tests/src/CoverageTest.php 进行测试。
PHPUnit 7.5.12 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 65 ms, Memory: 6.00 MB
OK (1 test, 5 assertions)
in Clover XML format ... done
目录会生成一个 CoverageTest.xml 文件。此时需要对其中的 XML 文件进行分析。我们不会进行人工分析,而是让 PHPStorm 来为我们解决。
单击 File 并选择 New Project 之后,再选择该工程。打开 CoverageTest.php 文件。单击 Run 并选择 Run CoverageTest With Coverage ,点击运行,之后你会看到下面这样的图。
相信你已经掌握了使用phpunit进行单元测试的方法了吧。