引言
先看个使用的例子:
上面是单元测试报错信息,下面是正常信息。
单元测试(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 编写测试的基本惯例与步骤:
- 针对类 Class 的测试写在类 StackTest 中。
- StackTest(通常)继承自 PHPUnit\Framework\TestCase。
- 测试都是命名为 test* 的公用方法。
- 也可以在方法的文档注释块(docblock)中使用 @test 标注将其标记为测试方法。
- 在测试方法内,类似于 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/