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

如何使用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进行单元测试的方法了吧。

你可能想看:
« 上一篇
下一篇 »

相关推荐

php实时推送,php实地培训

2026年10月05日

53阅读

php乱码出现问号的原因,php乱码出现问号的原因有哪些

2026年10月05日

44阅读

php一句话木马,一句话木马ctf

2026年10月05日

48阅读

php获取数组长度,php中如何获取数组的长度

2026年10月05日

50阅读

php面试题库及答案,php程序员面试真题库

2026年10月05日

47阅读

phpcontent-type修改,php源码二次修改需要多少钱

2026年10月05日

42阅读