PHP 单元测试


PHP 单元测试


引言

先看个使用的例子:

上面是单元测试报错信息,下面是正常信息。

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义, 一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。 总的来说,单元就是人为规定的最小的被测功能模块。 单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

单元测试是对单独的代码对象进行测试的过程,比如对函数、类、方法进行测试。单元测试可以使用任意一段已经写好的测试代码, 也可以使用一些已经存在的测试框架,比如JUnit、PHPUnit或者Cantata++,单元测试框架提供了一系列共同、有用的功能来帮助人们编写自动化的检测单元, 例如检查一个实际的值是否符合我们期望的值的断言。单元测试框架经常会包含每个测试的报告,以及给出你已经覆盖到的代码覆盖率。

PHPUnit 是一个用PHP编程语言开发的开源软件,是一个单元测试框架。 PHPUnit由Sebastian Bergmann创建,源于Kent Beck的SUnit,是xUnit家族的框架之一。

总之一句话,使用 phpunit 进行自动测试,会使你的代码更健壮,减少后期维护的成本,也是一种比较标准的规范, 现如今流行的PHP框架都带了单元测试,如Laraval、Symfony、Yii2等,单元测试已经成了标配。

另外,单元测试用例是通过命令操控测试脚本的,而不是通过浏览器访问URL的。

这里记录了一些PHPUnit的测试实例,包含各设计模式,可以看一下 https://github.com/iBaiYang/PHPUnitTset

安装PHPUnit

使用 composer 方式安装 PHPUnit:

composer require --dev phpunit/phpunit ^7

这里参阅下 https://ibaiyang.github.io/blog/php/2021/06/25/PHPUnit测试框架.html

引导示例

断言

这是 官方文档中的一个实例 http://phpunit.cn/manual/current/zh_cn/phpunit-book.html#writing-tests-for-phpunit

展示了如何用 PHPUnit 编写测试来对 PHP 数组操作进行测试。本例介绍了用 PHPUnit 编写测试的基本惯例与步骤:

  1. 针对类 Class 的测试写在类 StackTest 中。
  2. StackTest(通常)继承自 PHPUnit\Framework\TestCase。
  3. 测试都是命名为 test* 的公用方法。
  4. 也可以在方法的文档注释块(docblock)中使用 @test 标注将其标记为测试方法。
  5. 在测试方法内,类似于 assertEquals()(参见 附录 A)这样的断言方法用来对实际值与预期值的匹配做出断言。

创建目录tests,新建文件 StackTest.php,编辑如下:

<?php
namespace App\tests;

require_once __DIR__ . '/../vendor/autoload.php';

use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    public function testPushAndPop()
    {
        $stack = [];
        $this->assertEquals(0, count($stack));
        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertEquals(1, count($stack));
        $this->assertEquals('foo', array_pop($stack));
        $this->assertEquals(0, count($stack));
    }
}

StackTest为测试类,继承于PHPUnit\Framework\TestCase; 测试方法testPushAndPop(),测试方法必须为public权限。 在测试方法内,类似于 assertEquals() 这样的断言方法用来对实际值与预期值的匹配做出断言。

命令行执行:

framework# ./vendor/bin/phpunit tests/StackTest.php

或者可以省略文件后缀名:

framework# ./vendor/bin/phpunit tests/StackTest

执行结果:

framework# ./vendor/bin/phpunit tests/StackTest.php
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 56 ms, Memory: 4.00MB

OK (1 test, 5 assertions)

标注

代码 src/Email.php

<?php
declare(strict_types=1);

final class Email
{
    private $email;

    private function __construct(string $email)
    {
        $this->ensureIsValidEmail($email);

        $this->email = $email;
    }

    public static function fromString(string $email): self
    {
        return new self($email);
    }

    public function __toString(): string
    {
        return $this->email;
    }

    private function ensureIsValidEmail(string $email): void
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(
                sprintf(
                    '"%s" is not a valid email address',
                    $email
                )
            );
        }
    }
}

测试代码 tests/EmailTest.php

<?php
declare(strict_types=1);

use PHPUnit\Framework\TestCase;

/**
 * @covers Email
 */
final class EmailTest extends TestCase
{
    public function testCanBeCreatedFromValidEmailAddress(): void
    {
        $this->assertInstanceOf(
            Email::class,
            Email::fromString('user@example.com')
        );
    }

    public function testCannotBeCreatedFromInvalidEmailAddress(): void
    {
        $this->expectException(InvalidArgumentException::class);

        Email::fromString('invalid');
    }

    public function testCanBeUsedAsString(): void
    {
        $this->assertEquals(
            'user@example.com',
            Email::fromString('user@example.com')
        );
    }
}

编写 PHPUnit 测试

对于每个测试的运行,PHPUnit 命令行工具输出一个字符来指示进展:

  • . 当测试成功时输出。
  • F 当测试方法运行过程中一个断言失败时输出。
  • E 当测试方法运行过程中产生一个错误时输出。
  • R 当测试被标记为有风险时输出。
  • S 当测试被跳过时输出。
  • I 当测试被标记为不完整或未实现时输出。

PHPUnit 区分 失败(failure)与错误(error)。失败指的是被违背了的 PHPUnit 断言,例如一个失败的 assertEquals() 调用。 错误指的是意料之外的异常(exception)或 PHP 错误。 这种差异已被证明在某些时候是非常有用的,因为错误往往比失败更容易修复。 如果得到了一个非常长的问题列表,那么最好先对付错误,当错误全部修复了之后再试一次瞧瞧还有没有失败。

测试的依赖关系

用 @depends 标注来表达测试方法之间的依赖关系

数据供给器

测试方法可以接受任意参数。这些参数由数据供给器方法(在 例子 中,是 additionProvider() 方法)提供。 用 @dataProvider 标注来指定使用哪个数据供给器方法。

数据供给器方法必须声明为 public,其返回值要么是一个数组,其每个元素也是数组;要么是一个实现了 Iterator 接口的对象, 在对它进行迭代时每步产生一个数组。每个数组都是测试数据集的一部分,将以它的内容作为参数来调用测试方法。

对异常进行测试

用 @expectException 标注来测试被测代码中是否抛出了异常。

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    public function testException()
    {
        $this->expectException(InvalidArgumentException::class);
    }
}
?>
phpunit ExceptionTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

对 PHP 错误进行测试

默认情况下,PHPUnit 将测试在执行中触发的 PHP 错误、警告、通知都转换为异常。 利用这些异常,就可以,比如说,预期测试将触发 PHP 错误。

<?php
use PHPUnit\Framework\TestCase;

class ExpectedErrorTest extends TestCase
{
    /**
     * @expectedException PHPUnit\Framework\Error
     */
    public function testFailingInclude()
    {
        include 'not_existing_file.php';
    }
}
?>
phpunit -d error_reporting=2 ExpectedErrorTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)

对输出进行测试

有时候,想要断言(比如说)某方法的运行过程中生成了预期的输出(例如,通过 echo 或 print)。 PHPUnit\Framework\TestCase 类使用 PHP 的 输出缓冲 特性来为此提供必要的功能支持。

用 expectOutputString() 方法来设定所预期的输出。如果没有产生预期的输出,测试将计为失败。

<?php
use PHPUnit\Framework\TestCase;

class OutputTest extends TestCase
{
    public function testExpectFooActualFoo()
    {
        $this->expectOutputString('foo');
        print 'foo';
    }

    public function testExpectBarActualBaz()
    {
        $this->expectOutputString('bar');
        print 'baz';
    }
}
?>
phpunit OutputTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

.F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) OutputTest::testExpectBarActualBaz
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'


FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

错误相关信息的输出

当有测试失败时,PHPUnit 全力提供尽可能多的有助于找出问题所在的上下文信息。

例 1: 数组比较失败时生成的错误相关信息输出

<?php
use PHPUnit\Framework\TestCase;

class ArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2,  3, 4, 5, 6],
            [1, 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit ArrayDiffTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayDiffTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
     0 => 1
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )

/home/sb/ArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

在这个例子中,数组中只有一个值不同,但其他值也都同时显示出来,以提供关于错误发生的位置的上下文信息。

当生成的输出很长而难以阅读时,PHPUnit 将对其进行分割,并在每个差异附近提供少数几行上下文信息。

边缘情况

当比较失败时,PHPUnit 为输入值建立文本表示,然后以此进行对比。这种实现导致在差异指示中显示出来的问题可能比实际上存在的多。

这种情况只出现在对数组或者对象使用 assertEquals 或其他“弱”比较函数时。

例,当使用弱比较时在生成的差异结果中出现的边缘情况:

<?php
use PHPUnit\Framework\TestCase;

class ArrayWeakComparisonTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2, 3, 4, 5, 6],
            ['1', 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit ArrayWeakComparisonTest
PHPUnit 6.5.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayWeakComparisonTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
-    0 => 1
+    0 => '1'
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )


/home/sb/ArrayWeakComparisonTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

在这个例子中,第一个索引项中的 1 and ‘1’ 在报告中被视为不同,虽然 assertEquals 认为这两个值是匹配的。

PHPUnit 解剖

看一下./vendor/bin/phpunit脚本内容:

#!/usr/bin/env php
<?php
/*
 * This file is part of PHPUnit.
 *
 * (c) Sebastian Bergmann <sebastian@phpunit.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

if (version_compare('7.1.0', PHP_VERSION, '>')) {
    fwrite(
        STDERR,
        sprintf(
            'This version of PHPUnit is supported on PHP 7.1, PHP 7.2, and PHP 7.3.' . PHP_EOL .
            'You are using PHP %s (%s).' . PHP_EOL,
            PHP_VERSION,
            PHP_BINARY
        )
    );

    die(1);
}

if (!ini_get('date.timezone')) {
    ini_set('date.timezone', 'UTC');
}

foreach (array(__DIR__ . '/../../autoload.php', __DIR__ . '/../vendor/autoload.php', __DIR__ . '/vendor/autoload.php') as $file) {
    if (file_exists($file)) {
        define('PHPUNIT_COMPOSER_INSTALL', $file);

        break;
    }
}

unset($file);

if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
    fwrite(
        STDERR,
        'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL .
        '    composer install' . PHP_EOL . PHP_EOL .
        'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL
    );

    die(1);
}

$options = getopt('', array('prepend:'));

if (isset($options['prepend'])) {
    require $options['prepend'];
}

unset($options);

require PHPUNIT_COMPOSER_INSTALL;

PHPUnit\TextUI\Command::main();

测试执行

➜ phpunit src/Email.php tests/EmailTest
PHPUnit 6.3.0 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 70 ms, Memory: 10.00MB

OK (3 tests, 3 assertions)

tests/EmailTest 指示PHPUnit命令行测试要执行的测试 EmailTest 类声明在 tests/EmailTest.php 。

使用 tests 而不是 tests/EmailTest,将指示PHPUnit命令行执行所有已声明的测试 *Test.php 源代码文件在 tests 目录。

Yii2测试

Yii2 测试 可以学习使用下。






参考资料

PHPUnit 中文网 http://phpunit.cn/

PHPUnit https://phpunit.de/index.html

PHPUnit Github https://github.com/sebastianbergmann/phpunit

PHPUnit 手册 https://phpunit.readthedocs.io/zh_CN/latest/

w3c.PHPUnit 手册 https://www.w3cschool.cn/phpunit5/

单元测试PHPUnit入门三板斧 https://www.jianshu.com/p/c37e3e6305ee

PHP单元测试框架PHPUnit的使用方法 https://blog.csdn.net/moliyiran/article/details/84349446

Yii 2.0 权威指南 测试(Testing): 概述(Overview) https://www.yiichina.com/doc/guide/2.0/test-overview

《eBook - PHP 7 Explained (Everything you need to know about the next generation)》https://php7explained.com/


返回