安装 phpunit
项目安装
composer require --dev phpunit/phpunit
使用 ./vendor/bin/phpunit 执行单元测试
windows 平台
D:vendor/bin/phpunit
全局安装
composer global require --dev phpunit/phpunit
将全局的 vendor/bin 目录加入环境变量 Path,使用 phpunit 执行单元测试
快速入门
基本格式
针对类
Class的测试写在类ClassTest中(通常类ClassTest定义在文件ClassTest.php内)ClassTest类必须满足以下二个条件之一:要么它继承自PHPUnit\Framework\TestCase;要么它提供public static suite()方法,这个方法返回一个PHPUnit\Framework\Test对象。PHPUnit只会测试命名为test*的公用方法。也可以在方法的文档注释块(docblock)中使用
@test标注将其标记为测试方法。在测试方法内,使用类似于
assertTrue()这样的断言方法(更多断言方法见下文)用来对实际值与预期值的匹配做出断言
<?phpnamespace PHPUnitDemo;use PHPUnit\Framework\TestCase;class SampleTest extends TestCase{public function testSomething(){$this->assertTrue(true, 'This should already work.');}/*** @test*/public function something(){$this->assertTrue(true, 'This should already work.');}}
测试的依赖关系
PHPUnit支持对测试方法之间的显式依赖关系进行声明。
生产者(producer),是能生成被测单元并将其作为返回值的测试方法。
消费者(consumer),是依赖于一个或多个生产者及其返回值的测试方法。
通过在消费者方法的注释中使用 @depends 标注来声明依赖哪个生产者的返回值,可以使用多个 @depends 标注。拥有多个 @depends 标注的测试,其第一个参数是第一个生产者提供的基境,第二个参数是第二个生产者提供的基境,以此类推
PHPUnit 不会更改测试的运行顺序,因此你需要自行保证某个测试所依赖的所有测试均出现于这个测试之前。
默认情况下,生产者所产生的返回值将“原样”传递给相应的消费者。这意味着,如果生产者返回的是一个对象,那么传递给消费者的将是一个指向此对象的引用。如果需要传递对象的副本而非引用,则应当用
@depends clone
<?phpnamespace PHPUnitDemo;use PHPUnit\Framework\TestCase;class MultipleDependenciesTest extends TestCase{public function testProducerFirst(){$this->assertTrue(true);return 'first';}public function testProducerSecond(){$this->assertTrue(true);return 'second';}/*** @depends testProducerFirst* @depends testProducerSecond*/public function testConsumer($a, $b){$this->assertSame('first', $a);$this->assertSame('second', $b);}}
数据提供器
在依赖中,所依赖函数的返回值作为参数传入测试函数。除此之外,我们也可以用数据提供器来定义传入的数据。
数据提供器方法必须声明为 public,其返回值要么是一个数组,其每个元素也是数组;要么是一个实现了 Iterator 接口的对象,在对它进行迭代时每步产生一个数组。每个数组都是测试数据集的一部分,将以它的内容作为参数来调用测试方法。
通过在测试方法的注释中使用 @dataProvider 标注来指定使用哪个数据提供器方法。
<?phpnamespace PHPUnitDemo;use PHPUnit\Framework\TestCase;class DataTest extends TestCase{/*** @dataProvider additionProvider* @param $a* @param $b* @param $expected*/public function testAdd($a, $b, $expected){$this->assertSame($expected, $a + $b);}public function additionProvider(){return ['adding zeros' => [0, 0, 0], // 0 + 0 = 0 pass'zero plus one' => [0, 1, 1], // 0 + 1 = 1 pass'one plus zero' => [1, 0, 1], // 1 + 0 = 1 pass'one plus one' => [1, 1, 2], // 1 + 1 = 2 pass];}}
如果测试同时从 @dataProvider 方法和一个或多个 @depends 测试接收数据,那么来自于数据提供器的参数将先于来自所依赖的测试的。来自于所依赖的测试的参数对于每个数据集都是一样的
测试异常
使用 expectException()、expectExceptionCode()、expectExceptionMessage() 和 expectExceptionMessageRegExp() 方法为被测代码所抛出的异常建立预期。
需要在测试方法的开始处声明断言,然后执行语句。而不是调用后再声明
或者,也可以通过注释中使用 不赞成使用此方式,且 PHPUnit 9 中将要移除@expectedException、@expectedExceptionCode、@expectedExceptionMessage 和 @expectedExceptionMessageRegExp 标注为被测代码所抛出的异常建立预期。
<?phpnamespace PHPUnitDemo;use PHPUnit\Framework\TestCase;use Exception;class ExceptionTest extends TestCase{/*** @throws Exception*/public function testException(){$this->expectException(Exception::class);throw new Exception('test');}/*** @throws Exception* @expectedException Exception*/public function testExceptionExpect(){throw new Exception('test');}}
测试 PHP 错误
通过提前添加期望,来使测试正常进行,而不会报出 PHP 错误
<?phpnamespace PHPUnitDemo;use PHPUnit\Framework\TestCase;use PHPUnit\Framework\Error\Error;class ExpectedErrorTest extends TestCase{public function testFailingInclude(){$this->expectException(Error::class);include 'not_existing_file.php';}}
测试输出
有时候,想要断言(比如说)某方法的运行过程中生成了预期的输出(例如,通过 echo 或 print 等)
用 expectOutputString() 方法来设定所预期的输出。如果没有产生预期的输出,测试将计为失败。
<?phpnamespace PHPUnitDemo;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 命令行测试执行器通过 phpunit 命令调用
测试进展
对于每个测试的运行,PHPUnit 命令行工具输出一个字符来指示进展:
.当测试成功时输出。
F当测试方法运行过程中一个断言失败时输出。
E当测试方法运行过程中产生一个错误时输出。
R当测试被标记为有风险时输出
S当测试被跳过时输出
I当测试被标记为不完整或未实现时输出
PHPUnit 区分 败(failure)与错误(error)。失败指的是被违背了的 PHPUnit 断言,例如一个失败的 assertEquals() 调用。错误指的是意料之外的异常(exception)或 PHP 错误。这种差异已被证明在某些时候是非常有用的,因为错误往往比失败更容易修复。如果得到了一个非常长的问题列表,那么最好先对付错误,当错误全部修复了之后再试一次瞧瞧还有没有失败
命令行参数
phpunit UnitTest运行由
UnitTest类提供的测试。这个类应当在UnitTest.php源文件中声明。如果类
UnitTest不存在,则尝试将UnitTest作为文件夹解析,执行文件夹下的所有测试也可以使用文件
phpunit.xml或phpunit.xml.dist指定测试目录phpunit UnitTest UnitTest.php运行由
UnitTest类提供的测试。这个类应当在指定的源文件中声明。--bootstrap在测试前先运行一个 PHP 文件。例如运行一个
vendor/autoload.php自动加载类--configuration,-c从 XML 文件中读取配置信息。
如果
phpunit.xml或phpunit.xml.dist(按此顺序)存在于当前工作目录并且未使用--configuration,将自动从此文件中读取配置--no-configuration忽略当前工作目录下的
phpunit.xml与phpunit.xml.dist。
phpunit.xml
<phpunit bootstrap="vendor/autoload.php"><testsuites><testsuite name="money"><directory>tests</directory></testsuite></testsuites></phpunit>
<phpunit bootstrap="src/autoload.php"><testsuites><testsuite name="money"><file>tests/IntlFormatterTest.php</file><file>tests/MoneyTest.php</file><file>tests/CurrencyTest.php</file></testsuite></testsuites></phpunit>
基境
在编写测试时,最费时的部分之一是编写代码来将整个场景设置成某个已知的状态,并在测试结束后将其复原到初始状态。这个已知的状态称为测试的基境(fixture)。
每个测试方法都是在一个全新的测试类实例上运行的。测试类的每个测试方法都会运行一次 setUp() 和 tearDown() 模板方法
PHPUnit 支持共享建立基境的代码。在运行某个测试方法前,会调用一个名叫 setUp() 的模板方法。setUp() 是创建测试所用对象的地方。当测试方法运行结束后,不管是成功还是失败,都会调用另外一个名叫 tearDown() 的模板方法。tearDown() 是清理测试所用对象的地方
那为什么不直接用构造函数和析构函数呢?是因为
PHPUnit\Framework\TestCase类中已定义了构造函数用做其他用途,且不能被覆盖。当然你可可以直接用构造函数,然后再parent::__construct()
基境共享
一个有实际意义的多测试间共享基境的例子是数据库连接:只登录数据库一次,然后重用此连接,而不是每个测试都建立一个新的数据库连接。这样能加快测试的运行
setUpBeforeClass() 与 tearDownAfterClass() 模板方法将分别在测试用例类的第一个测试运行之前和测试用例类的最后一个测试运行之后调用,可以分别在测试用例类的第一个测试之前和最后一个测试之后连接与断开数据库
<?phpuse PHPUnit\Framework\TestCase;class DatabaseTest extends TestCase{protected static $dbh;public static function setUpBeforeClass(): void{self::$dbh = new PDO('sqlite::memory:');}public static function tearDownAfterClass(): void{self::$dbh = null;}}
所有可用模板方法
<?phpuse PHPUnit\Framework\TestCase;class TemplateMethodsTest extends TestCase{public static function setUpBeforeClass(): void{fwrite(STDOUT, __METHOD__ . "\n");}protected function setUp(): void{fwrite(STDOUT, __METHOD__ . "\n");}protected function assertPreConditions(): void{fwrite(STDOUT, __METHOD__ . "\n");}protected function assertPostConditions(): void{fwrite(STDOUT, __METHOD__ . "\n");}protected function tearDown(): void{fwrite(STDOUT, __METHOD__ . "\n");}public static function tearDownAfterClass(): void{fwrite(STDOUT, __METHOD__ . "\n");}protected function onNotSuccessfulTest(Throwable $e): void{fwrite(STDOUT, __METHOD__ . "\n");throw $e;}}
全局状态
使用单例和全局变量的代码很难测试。全局变量和类的静态属性都是一种全局状态
欲测代码和全局状态之间会强烈耦合,并且其创建无法控制。另外,一个测试对全局状态的改变可能会破坏另外一个测试。
在版本 6 之前,默认情况下,PHPUnit 用一种更改全局变量与超全局变量($GLOBALS、$_ENV、$_POST、$_GET、$_COOKIE、$_SERVER、$_FILES、$_REQUEST)不会影响到其他测试的方式来运行所有测试。
在版本 6 中,默认情况下 PHPUnit 不再对全局变量和超全局变量进行这种备份与恢复的操作。可以用 --globals-backup 选项或在 XML 配置文件中用 backupGlobals="true" 将其激活。
通过用 --static-backup 选项或在 XML 配置文件中设置 backupStaticAttributes="true",可以将此隔离扩展到类的静态属性。
对全局变量和类的静态属性的备份与还原操作使用了 serialize() 与 unserialize()。某些类的实例对象(比如 PDO)无法序列化,因此如果把这样一个对象存放在比如说 $GLOBALS 数组内时,备份操作就会出问题。
断言方法(部分)
编程术语中的断言是指判断系统中的某个假设是否成立的语句或方法。使用断言的常见做法是定义什么是什么的假设,,如果假设不成立,那么就会产生某种警告
| 方法 | 说明 |
|---|---|
assertEquals($val1, $val2, $message) |
$val1 与 $val2 不等时报告错误 |
assertFalse($expression, $message) |
$expression 不为 false 时报告错误 |
assertTrue($expression, $message) |
$expression 不为 true 时报告错误 |
assertNull($val, $message) |
$val 不为 null 时报告错误 |
assertNotNull($val, $message) |
$val 为 null 时报告错误 |
assertSame($val1, $val2, $message) |
$val1 和 $val2 不是同一个对象的引用,或者它们是不同类型的变量或值时报告错误 |
assertNotSame($val1, $val2, $message) |
$val1 和 $val2 是同一个对象的引用,或者它们是相同类型的变量或值时报告错误 |
assertRegExp($regexp, $val, $message) |
$val 不匹配正则表达式 $regexp 时报告错误 |
assertAttributeSame($val, $attribute, $classname, $message) |
$val 与 $classname::$attribute 是不同的类型或值时报告错误 |
expectException($exception) |
期待抛出一个 $exception 异常,不抛出时报告错误(需要在测试方法的开始处声明断言,然后执行语句。而不是调用后再声明) |
fail() |
测试方法直接报告错误 |
有风险的测试(Risky Tests)
无用测试(Useless Tests)
默认情况下,如果你的测试函数没有添加预期或者断言,就会被认为是无用测试。
通过设置
--dont-report-useless-tests命令行参数,或者在 xml 配置文件中配置beStrictAboutTestsThatDoNotTestAnything="false"来更改这一默认行为。
意外的代码覆盖(Unintentionally Covered Code)
当打开这个配置后,如果使用 @covers 注释来包含一些文件的覆盖报告,就会被判定为有风险的测试。
通过设置
--strict-coverage命令行参数,或者在 xml 配置文件中配置beStrictAboutCoversAnnotation="true"来更改这一默认行为。
测试过程中有输出(Output During Test Execution)
如果在测试过程中输出文本,则会被认定为有风险的测试。
通过设置
--disallow-test-output命令行参数,或者在 xml 配置文件中配置beStrictAboutOutputDuringTests="true"来更改这一默认行为。
测试超时(Test Execution Timeout)
通过注释来限制某些测试不能超过一定时间:
通过设置
--enforce-time-limit命令行参数,或者在 xml 配置文件中配置enforceTimeLimit="true"来更改这一默认行为。
操作全局状态(Global State Manipulation)
phpunit 可以对全局状态进行检测。
通过设置
--strict-global-state命令行参数,或者在 xml 配置文件中配置beStrictAboutChangesToGlobalState="true"来更改这一默认行为。
待完善的测试和跳过的测试
处于一些原因,我们希望跳过或者对某些测试方法标记未待完善
待完善的测试
开始写新的测试用例类时,可能想从写下空测试方法开始。
空测试的问题是 PHPUnit 框架会将它们解读为成功,假如把成功的测试视为绿灯、测试失败视为红灯,那么还额外需要黄灯来将测试标记为未完成或尚未实现
通过在测试方法中调用便捷方法 markTestIncomplete() 将这个测试标记为未完成
<?phpuse PHPUnit\Framework\TestCase;class SampleTest extends TestCase{public function testSomething(){// 可选:如果愿意,在这里随便测试点什么。$this->assertTrue(true, '这应该已经是能正常工作的。');// 在这里停止,并将此测试标记为未完成。$this->markTestIncomplete('此测试目前尚未实现。');}}
跳过的测试
并非所有测试都能在任何环境中运行。比如说,考虑这样一种情况:一个数据库抽象层,针对其所支持的各种数据库系统有多个不同的驱动程序。针对 MySQL 驱动程序的测试当然只在 MySQL 服务器可用才能运行
检查了 MySQLi 扩展是否可用,并且在扩展不可用时用 markTestSkipped() 方法来跳过此测试
use PHPUnit\Framework\TestCase;class DatabaseTest extends TestCase{protected function setUp(): void{if (!extension_loaded('mysqli')) {$this->markTestSkipped('The MySQLi extension is not available.');}}public function testConnection(){// ...}}
用 @requires 来跳过测试
除了上述方法,还可以用 @requires 标注来表达测试用例的一些常见前提条件。
| 类型 | 可能的值 | 范例 | 其他范例 |
|---|---|---|---|
PHP |
任何 PHP 版本标识符 | @requires PHP 5.3.3 | @requires PHP 7.1-dev |
PHPUnit |
任何 PHPUnit 版本标识符 | @requires PHPUnit 3.6.3 | @requires PHPUnit 4.6 |
OS |
用来对 PHP_OS 进行匹配的正则表达式 | @requires OS Linux | @requires OS WIN32{WINNT |
function |
任何对 function_exists 而言有效的参数 | @requires function imap_open | @requires function ReflectionMethod::setAccessible |
extension |
任何扩展模块名,可以附带有版本标识符 | @requires extension mysqli | @requires extension redis 2.2.0 |
<?phpuse PHPUnit\Framework\TestCase;/*** @requires extension mysqli*/class DatabaseTest extends TestCase{/*** @requires PHP 5.3*/public function testConnection(){// 测试要求有 mysqli 扩展,并且 PHP >= 5.3}// ... 所有其他要求有 mysqli 扩展的测试}