PHPUnit 手册php
版权 © 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015 Sebastian Bergmannhtml
此版本对应于 PHPUnit 5.0。最后更新于 2015-11-29。python
PHPUnit 5.0 须要 PHP 5.6,强烈推荐使用最新版本的 PHP。mysql
PHPUnit 须要使用 dom 和 json 扩展,它们一般是默认启用的。linux
PHPUnit 还须要 pcre、reflection、spl 扩展。这些标准扩展默认启用,而且除非修改 PHP 的构建系统和 C 源代码,不然没法禁用它们。git
代码覆盖率分析报告功能须要 Xdebug (2.1.3以上)与 tokenizer 扩展。生成 XML 格式的报告须要有 xmlwriter 扩展。github
要获取 PHPUnit,最简单的方法是下载 PHPUnit 的 PHP 档案包 (PHAR),它将 PHPUnit 所须要的全部必要组件(以及某些可选组件)捆绑在单个文件中:web
要使用 PHP档案包(PHAR)须要有 phar 扩展。正则表达式
要使用 PHAR 的 --self-update
功能须要有 openssl 扩展。
若是启用了 Suhosin 扩展,须要在 php.ini
中容许执行 PHAR:
suhosin.executor.include.whitelist = phar
要从 https://phar.phpunit.de/
下载,须要支持 TLS/SNI的客户端,例如 wget 1.14(或更高版本)。
若是要全局安装 PHAR:
$wget https://phar.phpunit.de/phpunit.phar$chmod +x phpunit.phar$sudo mv phpunit.phar /usr/local/bin/phpunit$phpunit --versionPHPUnit x.y.z by Sebastian Bergmann and contributors.
也能够直接使用下载的 PHAR 文件:
$wget https://phar.phpunit.de/phpunit.phar$php phpunit.phar --versionPHPUnit x.y.z by Sebastian Bergmann and contributors.
总体上说,在 Windows 下安装 PHAR 和手工在 Windows 下安装 Composer 是同样的过程:
为 PHP 的二进制可执行文件创建一个目录,例如 C:\bin
将 ;C:\bin
附加到 PATH
环境变量中(相关帮助)
下载 https://phar.phpunit.de/phpunit.phar 并将文件保存到 C:\bin\phpunit.phar
打开命令行(例如,按 Windows+R » 输入 cmd
» ENTER)
创建外包覆批处理脚本(最后获得 C:\bin\phpunit.cmd
):
C:\Users\username>cd C:\binC:\bin>echo @php "%~dp0phpunit.phar" %* > phpunit.cmdC:\bin>exit
新开一个命令行窗口,确认一下能够在任意路径下执行 PHPUnit:
C:\Users\username>phpunit --versionPHPUnit x.y.z by Sebastian Bergmann and contributors.
对于 Cygwin 或 MingW32 (例如 TortoiseGit) shell 环境,能够跳过第五步。 取而代之的是,把文件保存为 phpunit
(没有 .phar
扩展名),而后用 chmod 775 phpunit
将其设为可执行。
由 PHPUnit 项目分发的全部官方代码发行包都由发行包管理器进行签名。在 phar.phpunit.de 上有 PGP 签名和 SHA1 散列值可用于校验。
下面的例子详细说明了如何对发行包进行校验。首先下载 phpunit.phar
和与之对应的单独 PGP 签名 phpunit.phar.asc
:
wget https://phar.phpunit.de/phpunit.pharwget https://phar.phpunit.de/phpunit.phar.asc
用单独的签名(phpunit.phar
)对 PHPUnit 的 PHP 档案包(phpunit.phar.asc
)进行校验:
gpg: Signature made Sat 19 Jul 2014 01:28:02 PM CEST using RSA key ID 6372C20A gpg: Can't check signature: public key not foundgpg phpunit.phar.asc
在本地系统中没有发行包管理器的公钥(6372C20A
)。为了能进行校验,必须从某个密钥服务器上取得发行包管理器的公钥。其中一个服务器是 pgp.uni-mainz.de
。全部密钥服务器是连接在一块儿的,所以链接到任一密钥服务器均可以。
gpg: requesting key 6372C20A from hkp server pgp.uni-mainz.de gpg: key 6372C20A: public key "Sebastian Bergmann <sb@sebastian-bergmann.de>" imported gpg: Total number processed: 1 gpg: imported: 1 (RSA: 1)gpg --keyserver pgp.uni-mainz.de --recv-keys 0x4AA394086372C20A
如今已经取得了条目名称为"Sebastian Bergmann <sb@sebastian-bergmann.de>"的公钥。不过没法检验这个密钥确实是由名叫 Sebastian Bergmann 的人建立的。可是能够先试着校验发行包的签名:
gpg: Signature made Sat 19 Jul 2014 01:28:02 PM CEST using RSA key ID 6372C20A gpg: Good signature from "Sebastian Bergmann <sb@sebastian-bergmann.de>" gpg: aka "Sebastian Bergmann <sebastian@php.net>" gpg: aka "Sebastian Bergmann <sebastian@thephp.cc>" gpg: aka "Sebastian Bergmann <sebastian@phpunit.de>" gpg: aka "Sebastian Bergmann <sebastian.bergmann@thephp.cc>" gpg: aka "[jpeg image of size 40635]" gpg: WARNING: This key is not certified with a trusted signature! gpg: There is no indication that the signature belongs to the owner. Primary key fingerprint: D840 6D0D 8294 7747 2937 7831 4AA3 9408 6372 C20Agpg phpunit.phar.asc
此时,签名已经没问题了,可是这个公钥还不能信任。签名没问题意味着文件未被篡改。但是因为公钥加密系统的性质,还须要再校验密钥 6372C20A
确实是由真正的 Sebastian Bergmann 建立的。
任何攻击者都能建立公钥并将其上传到公钥服务器。他们能够创建一个带恶意的发行包,并用这个假密钥进行签名。这样,若是尝试对这个损坏了的发行包进行签名校验,因为密钥是“真”密钥,校验将成功完成。所以,须要对这个密钥的真实性进行校验。如何对公钥的真实性进行校验已经超出了本文档的范畴。
有个比较谨慎的作法是建立一个脚原本管理 PHPUnit 的安装,在运行测试套件以前校验 GnuPG 签名。例如:
#!/usr/bin/env bash clean=1 # 是否在测试完成以后删除 phpunit.phar ? aftercmd="php phpunit.phar --bootstrap bootstrap.php src/tests" gpg --fingerprint D8406D0D82947747293778314AA394086372C20A if [ $? -ne 0 ]; then echo -e "\033[33mDownloading PGP Public Key...\033[0m" gpg --recv-keys D8406D0D82947747293778314AA394086372C20A # Sebastian Bergmann <sb@sebastian-bergmann.de> gpg --fingerprint D8406D0D82947747293778314AA394086372C20A if [ $? -ne 0 ]; then echo -e "\033[31mCould not download PGP public key for verification\033[0m" exit fi fi if [ "$clean" -eq 1 ]; then # 若是存在就清理掉 if [ -f phpunit.phar ]; then rm -f phpunit.phar fi if [ -f phpunit.phar.asc ]; then rm -f phpunit.phar.asc fi fi # 抓取最新的发行版和对应的签名 if [ ! -f phpunit.phar ]; then wget https://phar.phpunit.de/phpunit.phar fi if [ ! -f phpunit.phar.asc ]; then wget https://phar.phpunit.de/phpunit.phar.asc fi # 在运行前先校验 gpg --verify phpunit.phar.asc phpunit.phar if [ $? -eq 0 ]; then echo echo -e "\033[33mBegin Unit Testing\033[0m" # 运行测试套件 `$after_cmd` # 清理 if [ "$clean" -eq 1 ]; then echo -e "\033[32mCleaning Up!\033[0m" rm -f phpunit.phar rm -f phpunit.phar.asc fi else echo chmod -x phpunit.phar mv phpunit.phar /tmp/bad-phpunit.phar mv phpunit.phar.asc /tmp/bad-phpunit.phar.asc echo -e "\033[31mSignature did not match! PHPUnit has been moved to /tmp/bad-phpunit.phar\033[0m" exit 1 fi
若是用 Composer 来管理项目的依赖关系,只要在项目的 composer.json
文件中简单地加上对 phpunit/phpunit
的依赖关系便可。下面是一个最小化的 composer.json
文件的例子,只定义了一个对 PHPUnit 5.0 的开发时(development-time)依赖:
{ "require-dev": { "phpunit/phpunit": "5.0.*" } }
要经过 Composer 完成系统级的安装,能够运行:
composer global require "phpunit/phpunit=5.0.*"
请确保 path 变量中包含有 ~/.composer/vendor/bin/
。
有如下可选组件包可用:
PHP_Invoker
一个工具类,能够用带有超时限制的方式调用可调用内容。当须要在严格模式下保证测试的超时限制时,这个组件包是必须的。
PHPUnit 的 PHAR 分发中已经包含了此组件包。若要经过 Composer 安装此组件包,添加以下 "require-dev"
依赖项:
"phpunit/php-invoker": "*"
DbUnit
移植到 PHP/PHPUnit 上的 DbUnit 用于提供对数据库交互测试的支持。
PHPUnit 的 PHAR 分发中已经包含了此组件包。若要经过 Composer 安装此组件包,添加以下 "require-dev"
依赖项:
"phpunit/dbunit": ">=1.2"
例 2.1展现了如何用 PHPUnit 编写测试来对 PHP 数组操做进行测试。本例介绍了用 PHPUnit 编写测试的基本惯例与步骤:
针对类 Class
的测试写在类 ClassTest
中。
ClassTest
(一般)继承自 PHPUnit_Framework_TestCase
。
测试都是命名为 test*
的公用方法。
在测试方法内,相似于 assertEquals()
(参见 附录 A)这样的断言方法用来对实际值与预期值的匹配作出断言。
例 2.1: 用 PHPUnit 测试数组操做
<?php class StackTest extends PHPUnit_Framework_TestCase { public function testPushAndPop() { $stack = array(); $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)); } } ?>
当你想把一些东西写到 |
||
--Martin Fowler |
单元测试主要是做为一种良好实践来编写的,它能帮助开发人员识别并修复 bug、重构代码,还能够看做被测软件单元的文档。要实现这些好处,理想的单元测试应当覆盖程序中全部可能的路径。一个单元测试一般覆盖一个函数或方法中的一个特定路径。可是,测试方法并不必定非要是一个封装良好的独立实体。测试方法之间常常有隐含的依赖关系暗藏在测试的实现方案中。 |
||
--Adrian Kuhn et. al. |
PHPUnit支持对测试方法之间的显式依赖关系进行声明。这种依赖关系并非定义在测试方法的执行顺序中,而是容许生产者(producer)返回一个测试基境(fixture)的实例,并将此实例传递给依赖于它的消费者(consumer)们。
生产者(producer),是能生成被测单元并将其做为返回值的测试方法。
消费者(consumer),是依赖于一个或多个生产者及其返回值的测试方法。
例 2.2展现了如何用 @depends
标注来表达测试方法之间的依赖关系。
例 2.2: 用 @depends
标注来表达依赖关系
<?php class StackTest extends PHPUnit_Framework_TestCase { public function testEmpty() { $stack = array(); $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); } } ?>
在上例中,第一个测试, testEmpty()
,建立了一个新数组,并断言其为空。随后,此测试将此基境做为结果返回。第二个测试,testPush()
,依赖于 testEmpty()
,并将所依赖的测试之结果做为参数传入。最后,testPop()
依赖于 testPush()
。
默认状况下,生产者所产生的返回值将“原样”传递给相应的消费者。这意味着,若是生产者返回的是一个对象,那么传递给消费者的将是一个指向此对象的引用。若是须要传递对象的副本而非引用,则应当用 @depends clone
替代 @depends
。
为了快速定位缺陷,咱们但愿把注意力集中于相关的失败测试上。这就是为何当某个测试所依赖的测试失败时,PHPUnit 会跳过这个测试。经过利用测试之间的依赖关系,缺陷定位获得了改进,如例 2.3中所示。
例 2.3: 利用测试之间的依赖关系
<?php class DependencyFailureTest extends PHPUnit_Framework_TestCase { public function testOne() { $this->assertTrue(FALSE); } /** * @depends testOne */ public function testTwo() { } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. FS Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) DependencyFailureTest::testOne Failed asserting that false is true. /home/sb/DependencyFailureTest.php:6 There was 1 skipped test: 1) DependencyFailureTest::testTwo This test depends on "DependencyFailureTest::testOne" to pass. FAILURES! Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.phpunit --verbose DependencyFailureTest
测试能够使用多个 @depends
标注。PHPUnit 不会更改测试的运行顺序,所以你须要自行保证某个测试所依赖的全部测试均出现于这个测试以前。
拥有多个 @depends
标注的测试,其第一个参数是第一个生产者提供的基境,第二个参数是第二个生产者提供的基境,以此类推。参见例 2.4
例 2.4: 有多重依赖的测试
<?php class MultipleDependenciesTest extends PHPUnit_Framework_TestCase { public function testProducerFirst() { $this->assertTrue(true); return 'first'; } public function testProducerSecond() { $this->assertTrue(true); return 'second'; } /** * @depends testProducerFirst * @depends testProducerSecond */ public function testConsumer() { $this->assertEquals( array('first', 'second'), func_get_args() ); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. ... Time: 0 seconds, Memory: 3.25Mb OK (3 tests, 3 assertions)phpunit --verbose MultipleDependenciesTest
测试方法能够接受任意参数。这些参数由数据供给器方法(在 例 2.5中,是 additionProvider()
方法)提供。用 @dataProvider
标注来指定使用哪一个数据供给器方法。
数据供给器方法必须声明为 public
,其返回值要么是一个数组,其每一个元素也是数组;要么是一个实现了 Iterator
接口的对象,在对它进行迭代时每步产生一个数组。每一个数组都是测试数据集的一部分,将以它的内容做为参数来调用测试方法。
例 2.5: 使用返回数组的数组的数据供给器
<?php class DataTest extends PHPUnit_Framework_TestCase { /** * @dataProvider additionProvider */ public function testAdd($a, $b, $expected) { $this->assertEquals($expected, $a + $b); } public function additionProvider() { return array( array(0, 0, 0), array(0, 1, 1), array(1, 0, 1), array(1, 1, 3) ); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. ...F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) DataTest::testAdd with data set #3 (1, 1, 3) Failed asserting that 2 matches expected 3. /home/sb/DataTest.php:9 FAILURES! Tests: 4, Assertions: 4, Failures: 1.phpunit DataTest
当使用到大量数据集时,最好逐个用字符串键名对其命名,避免用默认的数字键名。这样输出信息会更加详细些,其中将包含打断测试的数据集所对应的名称。
例 2.6: 使用带有命名数据集的数据供给器
<?php class DataTest extends PHPUnit_Framework_TestCase { /** * @dataProvider additionProvider */ public function testAdd($a, $b, $expected) { $this->assertEquals($expected, $a + $b); } public function additionProvider() { return array( 'adding zeros' => array(0, 0, 0), 'zero plus one' => array(0, 1, 1), 'one plus zero' => array(1, 0, 1), 'one plus one' => array(1, 1, 3) ); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. ...F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) DataTest::testAdd with data set "one plus one" (1, 1, 3) Failed asserting that 2 matches expected 3. /home/sb/DataTest.php:9 FAILURES! Tests: 4, Assertions: 4, Failures: 1.phpunit DataTest
例 2.7: 使用返回迭代器对象的数据供给器
<?php require 'CsvFileIterator.php'; class DataTest extends PHPUnit_Framework_TestCase { /** * @dataProvider additionProvider */ public function testAdd($a, $b, $expected) { $this->assertEquals($expected, $a + $b); } public function additionProvider() { return new CsvFileIterator('data.csv'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. ...F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) DataTest::testAdd with data set #3 ('1', '1', '3') Failed asserting that 2 matches expected '3'. /home/sb/DataTest.php:11 FAILURES! Tests: 4, Assertions: 4, Failures: 1.phpunit DataTest
例 2.8: CsvFileIterator 类
<?php class CsvFileIterator implements Iterator { protected $file; protected $key = 0; protected $current; public function __construct($file) { $this->file = fopen($file, 'r'); } public function __destruct() { fclose($this->file); } public function rewind() { rewind($this->file); $this->current = fgetcsv($this->file); $this->key = 0; } public function valid() { return !feof($this->file); } public function key() { return $this->key; } public function current() { return $this->current; } public function next() { $this->current = fgetcsv($this->file); $this->key++; } } ?>
若是测试同时从 @dataProvider
方法和一个或多个 @depends
测试接收数据,那么来自于数据供给器的参数将先于来自所依赖的测试的。来自于所依赖的测试的参数对于每一个数据集都是同样的。参见例 2.9
例 2.9: 在同一个测试中组合使用 @depends 和 @dataProvider
<?php class DependencyAndDataProviderComboTest extends PHPUnit_Framework_TestCase { public function provider() { return array(array('provider1'), array('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 testConsumer() { $this->assertEquals( array('provider1', 'first', 'second'), func_get_args() ); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. ...F Time: 0 seconds, Memory: 3.50Mb There was 1 failure: 1) DependencyAndDataProviderComboTest::testConsumer with data set #1 ('provider2') Failed asserting that two arrays are equal. --- Expected +++ Actual @@ @@ Array ( - 0 => 'provider1' + 0 => 'provider2' 1 => 'first' 2 => 'second' ) /home/sb/DependencyAndDataProviderComboTest.php:31 FAILURES! Tests: 4, Assertions: 4, Failures: 1. phpunit --verbose DependencyAndDataProviderComboTest
例 2.10展现了如何用 @expectedException
标注来测试被测代码中是否抛出了异常。
例 2.10: 使用 @expectedException 标注
<?php class ExceptionTest extends PHPUnit_Framework_TestCase { /** * @expectedException InvalidArgumentException */ public function testException() { } } ?>
PHPUnit 5.0.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.phpunit ExceptionTest
另外,你能够将 @expectedExceptionMessage
、@expectedExceptionMessageRegExp
和 @expectedExceptionCode
与 @expectedException
联合使用,来对异常的讯息与代号进行测试,如例 2.11所示。
例 2.11: 使用 @expectedExceptionMessage
、@expectedExceptionMessageRegExp
和 @expectedExceptionCode
标注
<?php class ExceptionTest extends PHPUnit_Framework_TestCase { /** * @expectedException InvalidArgumentException * @expectedExceptionMessage Right Message */ public function testExceptionHasRightMessage() { throw new InvalidArgumentException('Some Message', 10); } /** * @expectedException InvalidArgumentException * @expectedExceptionMessageRegExp #Right.*# */ public function testExceptionMessageMatchesRegExp() { throw new InvalidArgumentException('Some Message', 10); } /** * @expectedException InvalidArgumentException * @expectedExceptionCode 20 */ public function testExceptionHasRightCode() { throw new InvalidArgumentException('Some Message', 10); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. FFF Time: 0 seconds, Memory: 3.00Mb There were 3 failures: 1) ExceptionTest::testExceptionHasRightMessage Failed asserting that exception message 'Some Message' contains 'Right Message'. 2) ExceptionTest::testExceptionMessageMatchesRegExp Failed asserting that exception message 'Some Message' matches '#Right.*#'. 3) ExceptionTest::testExceptionHasRightCode Failed asserting that expected exception code 20 is equal to 10. FAILURES! Tests: 3, Assertions: 6, Failures: 3.phpunit ExceptionTest
关于 @expectedExceptionMessage
、@expectedExceptionMessageRegExp
和 @expectedExceptionCode
,分别在“@expectedExceptionMessage”一节、“@expectedExceptionMessageRegExp”一节 和 “@expectedExceptionCode”一节有更多相关范例。
此外,还能够用 setExpectedException()
或 setExpectedExceptionRegExp()
方法来设定所预期的异常,如例 2.12所示。
例 2.12: 预期被测代码将引起异常
<?php class ExceptionTest extends PHPUnit_Framework_TestCase { public function testException() { $this->setExpectedException('InvalidArgumentException'); } public function testExceptionHasRightMessage() { $this->setExpectedException( 'InvalidArgumentException', 'Right Message' ); throw new InvalidArgumentException('Some Message', 10); } public function testExceptionMessageMatchesRegExp() { $this->setExpectedExceptionRegExp( 'InvalidArgumentException', '/Right.*/', 10 ); throw new InvalidArgumentException('The Wrong Message', 10); } public function testExceptionHasRightCode() { $this->setExpectedException( 'InvalidArgumentException', 'Right Message', 20 ); throw new InvalidArgumentException('The Right Message', 10); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. FFFF Time: 0 seconds, Memory: 3.00Mb There were 4 failures: 1) ExceptionTest::testException Expected exception InvalidArgumentException 2) ExceptionTest::testExceptionHasRightMessage Failed asserting that exception message 'Some Message' contains 'Right Message'. 3) ExceptionTest::testExceptionMessageMatchesRegExp Failed asserting that exception message 'The Wrong Message' contains '/Right.*/'. 4) ExceptionTest::testExceptionHasRightCode Failed asserting that expected exception code 20 is equal to 10. FAILURES! Tests: 4, Assertions: 8, Failures: 4.phpunit ExceptionTest
表 2.1中列举了用于对异常进行测试的各类方法。
表 2.1. 用于对异常进行测试的方法
方法 | 含义 |
---|---|
void setExpectedException(string $exceptionName[, string $exceptionMessage = '', integer $exceptionCode = NULL]) |
设定预期的 $exceptionName 、$exceptionMessage 和 $exceptionCode 。 |
void setExpectedExceptionRegExp(string $exceptionName[, string $exceptionMessageRegExp = '', integer $exceptionCode = NULL]) |
设定预期的 $exceptionName 、$exceptionMessageRegExp 和 $exceptionCode 。 |
String getExpectedException() |
返回预期异常的名称。 |
能够用 例 2.13 中所示方法来对异常进行测试。
例 2.13: 另外一种对异常进行测试的方法
<?php class ExceptionTest extends PHPUnit_Framework_TestCase { public function testException() { try { // ... 预期会引起异常的代码 ... } catch (InvalidArgumentException $expected) { return; } $this->fail('预期的异常未出现。'); } } ?>
当例 2.13 中预期会引起异常的代码并无引起异常时,后面对 fail()
的调用将会停止测试,并通告测试有问题。若是预期的异常出现了,将执行 catch
代码块,测试将会成功结束。
默认状况下,PHPUnit 将测试在执行中触发的 PHP 错误、警告、通知都转换为异常。利用这些异常,就能够,好比说,预期测试将触发 PHP 错误,如例 2.14所示。
PHP 的 error_reporting
运行时配置会对 PHPUnit 将哪些错误转换为异常有所限制。若是在这个特性上碰到问题,请确认 PHP 的配置中没有抑制想要测试的错误类型。
例 2.14: 用 @expectedException 来预期 PHP 错误
<?php class ExpectedErrorTest extends PHPUnit_Framework_TestCase { /** * @expectedException PHPUnit_Framework_Error */ public function testFailingInclude() { include 'not_existing_file.php'; } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. . Time: 0 seconds, Memory: 5.25Mb OK (1 test, 1 assertion)phpunit -d error_reporting=2 ExpectedErrorTest
PHPUnit_Framework_Error_Notice
和 PHPUnit_Framework_Error_Warning
分别表明 PHP 通知与 PHP 警告。
对异常进行测试是越明确越好的。对太笼统的类进行测试有可能致使不良反作用。所以,再也不容许用 @expectedException
或 setExpectedException()
对 Exception
类进行测试。
若是测试依靠会触发错误的 PHP 函数,例如 fopen
,有时候在测试中使用错误抑制符会颇有用。经过抑制住错误通知,就能对返回值进行检查,不然错误通知将会致使抛出 PHPUnit_Framework_Error_Notice
。
例 2.15: 对会引起PHP 错误的代码的返回值进行测试
<?php class ErrorSuppressionTest extends PHPUnit_Framework_TestCase { public function testFileWriting() { $writer = new FileWriter; $this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff')); } } class FileWriter { public function write($file, $content) { $file = fopen($file, 'w'); if($file == false) { return false; } // ... } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. . Time: 1 seconds, Memory: 5.25Mb OK (1 test, 1 assertion)phpunit ErrorSuppressionTest
若是不使用错误抑制符,此测试将会失败,并报告 fopen(/is-not-writeable/file): failed to open stream: No such file or directory
。
有时候,想要断言(好比说)某方法的运行过程当中生成了预期的输出(例如,经过 echo
或 print
)。PHPUnit_Framework_TestCase
类使用 PHP 的 输出缓冲 特性来为此提供必要的功能支持。
例 2.16展现了如何用 expectOutputString()
方法来设定所预期的输出。若是没有产生预期的输出,测试将计为失败。
例 2.16: 对函数或方法的输出进行测试
<?php class OutputTest extends PHPUnit_Framework_TestCase { public function testExpectFooActualFoo() { $this->expectOutputString('foo'); print 'foo'; } public function testExpectBarActualBaz() { $this->expectOutputString('bar'); print 'baz'; } } ?>
PHPUnit 5.0.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 OutputTest
表 2.2中列举了用于对输出进行测试的各类方法。
表 2.2. 用于对输出进行测试的方法
方法 | 含义 |
---|---|
void expectOutputRegex(string $regularExpression) |
设置输出预期为输出应当匹配正则表达式 $regularExpression 。 |
void expectOutputString(string $expectedString) |
设置输出预期为输出应当与 $expectedString 字符串相等。 |
bool setOutputCallback(callable $callback) |
设置回调函数,用来作诸如将实际输出规范化之类的动做。 |
在严格模式下,自己产生输出的测试将会失败。
当有测试失败时,PHPUnit 全力提供尽量多的有助于找出问题所在的上下文信息。
例 2.17: 数组比较失败时生成的错误相关信息输出
<?php class ArrayDiffTest extends PHPUnit_Framework_TestCase { public function testEquality() { $this->assertEquals( array(1,2,3 ,4,5,6), array(1,2,33,4,5,6) ); } } ?>
PHPUnit 5.0.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 ArrayDiffTest
在这个例子中,数组中只有一个值不一样,但其余值也都同时显示出来,以提供关于错误发生的位置的上下文信息。
当生成的输出很长而难以阅读时,PHPUnit 将对其进行分割,并在每一个差别附近提供少数几行上下文信息。
例 2.18: 长数组比较失败时生成的错误相关信息输出
<?php class LongArrayDiffTest extends PHPUnit_Framework_TestCase { public function testEquality() { $this->assertEquals( array(0,0,0,0,0,0,0,0,0,0,0,0,1,2,3 ,4,5,6), array(0,0,0,0,0,0,0,0,0,0,0,0,1,2,33,4,5,6) ); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) LongArrayDiffTest::testEquality Failed asserting that two arrays are equal. --- Expected +++ Actual @@ @@ 13 => 2 - 14 => 3 + 14 => 33 15 => 4 16 => 5 17 => 6 ) /home/sb/LongArrayDiffTest.php:7 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit LongArrayDiffTest
当比较失败时,PHPUnit 为输入值创建文本表示,而后以此进行对比。这种实现致使在差别指示中显示出来的问题可能比实际上存在的多。
这种状况只出如今对数组或者对象使用 assertEquals 或其余“弱”比较函数时。
例 2.19: 当使用弱比较时在生成的差别结果中出现的边缘状况
<?php class ArrayWeakComparisonTest extends PHPUnit_Framework_TestCase { public function testEquality() { $this->assertEquals( array(1 ,2,3 ,4,5,6), array('1',2,33,4,5,6) ); } } ?>
PHPUnit 5.0.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.phpunit ArrayWeakComparisonTest
在这个例子中,第一个索引项中的 1
and '1'
在报告中被视为不一样,虽然 assertEquals 认为这两个值是匹配的。
PHPUnit 命令行测试执行器可经过 phpunit
命令调用。下面的代码展现了如何用 PHPUnit 命令行测试执行器来运行测试:
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. .. Time: 0 seconds OK (2 tests, 2 assertions)phpunit ArrayTest
上面这个调用例子中,PHPUnit 命令行测试执行器将在当前工做目录中寻找 ArrayTest.php
源文件并加载之。而在此源文件中应当能找到 ArrayTest
测试用例类,此类中的测试将被执行。
对于每一个测试的运行,PHPUnit 命令行工具输出一个字符来指示进展:
PHPUnit 区分 败(failure)与错误(error)。失败指的是被违背了的 PHPUnit 断言,例如一个失败的 assertEquals()
调用。错误指的是意料以外的异常(exception)或 PHP 错误。这种差别已被证实在某些时候是很是有用的,由于错误每每比失败更容易修复。若是获得了一个很是长的问题列表,那么最好先对付错误,当错误所有修复了以后再试一次瞧瞧还有没有失败。
让咱们来瞧瞧如下代码中命令行测试运行器的各类选项:
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. Usage: phpunit [options] UnitTest [UnitTest.php] phpunit [options] <directory> Code Coverage Options: --coverage-clover <file> Generate code coverage report in Clover XML format. --coverage-crap4j <file> Generate code coverage report in Crap4J XML format. --coverage-html <dir> Generate code coverage report in HTML format. --coverage-php <file> Export PHP_CodeCoverage object to file. --coverage-text=<file> Generate code coverage report in text format. Default: Standard output. --coverage-xml <dir> Generate code coverage report in PHPUnit XML format. Logging Options: --log-junit <file> Log test execution in JUnit XML format to file. --log-tap <file> Log test execution in TAP format to file. --log-json <file> Log test execution in JSON format. --testdox-html <file> Write agile documentation in HTML format to file. --testdox-text <file> Write agile documentation in Text format to file. Test Selection Options: --filter <pattern> Filter which tests to run. --testsuite <pattern> Filter which testsuite to run. --group ... Only runs tests from the specified group(s). --exclude-group ... Exclude tests from the specified group(s). --list-groups List available test groups. --test-suffix ... Only search for test in files with specified suffix(es). Default: Test.php,.phpt Test Execution Options: --report-useless-tests Be strict about tests that do not test anything. --strict-coverage Be strict about unintentionally covered code. --strict-global-state Be strict about changes to global state --disallow-test-output Be strict about output during tests. --enforce-time-limit Enforce time limit based on test size. --disallow-todo-tests Disallow @todo-annotated tests. --process-isolation Run each test in a separate PHP process. --no-globals-backup Do not backup and restore $GLOBALS for each test. --static-backup Backup and restore static attributes for each test. --colors=<flag> Use colors in output ("never", "auto" or "always"). --columns <n> Number of columns to use for progress output. --columns max Use maximum number of columns for progress output. --stderr Write to STDERR instead of STDOUT. --stop-on-error Stop execution upon first error. --stop-on-failure Stop execution upon first error or failure. --stop-on-risky Stop execution upon first risky test. --stop-on-skipped Stop execution upon first skipped test. --stop-on-incomplete Stop execution upon first incomplete test. -v|--verbose Output more verbose information. --debug Display debugging information during test execution. --loader <loader> TestSuiteLoader implementation to use. --repeat <times> Runs the test(s) repeatedly. --tap Report test execution progress in TAP format. --testdox Report test execution progress in TestDox format. --printer <printer> TestListener implementation to use. Configuration Options: --bootstrap <file> A "bootstrap" PHP file that is run before the tests. -c|--configuration <file> Read configuration from XML file. --no-configuration Ignore default configuration file (phpunit.xml). --include-path <path(s)> Prepend PHP's include_path with given path(s). -d key[=value] Sets a php.ini value. Miscellaneous Options: -h|--help Prints this usage information. --version Prints the version and exits.phpunit --help
phpunit UnitTest
运行由 UnitTest
类提供的测试。这个类应当在 UnitTest.php
源文件中声明。
UnitTest
这个类必须知足如下二个条件之一:要么它继承自 PHPUnit_Framework_TestCase
;要么它提供 public static suite()
方法,这个方法返回一个 PHPUnit_Framework_Test
对象,好比,一个 PHPUnit_Framework_TestSuite
类的实例。
phpunit UnitTest UnitTest.php
运行由 UnitTest
类提供的测试。这个类应当在指定的源文件中声明。
--coverage-clover
为运行的测试生成带有代码覆盖率信息的 XML 格式的日志文件。更多细节请参见第 13 章。
请注意,此功能仅当安装了 tokenizer 和 Xdebug 这两个 PHP 扩展后才可用。
--coverage-crap4j
生成 Crap4j 格式的代码覆盖率报告。更多细节请参见第 11 章。
请注意,此功能仅当安装了 tokenizer 和 Xdebug 这两个 PHP 扩展后才可用。
--coverage-html
生成 HTML 格式的代码覆盖率报告。更多细节请参见 第 11 章。
请注意,此功能仅当安装了 tokenizer 和 Xdebug 这两个 PHP 扩展后才可用。
--coverage-php
生成一个序列化后的 PHP_CodeCoverage 对象,此对象含有代码覆盖率信息。
请注意,此功能仅当安装了 tokenizer 和 Xdebug 这两个 PHP 扩展后才可用。
--coverage-text
为运行的测试以人们可读的格式生成带有代码覆盖率信息的日志文件或命令行输出。更多细节请参见 第 13 章。
请注意,此功能仅当安装了 tokenizer 和 Xdebug 这两个 PHP 扩展后才可用。
--log-junit
为运行的测试生成 JUnit XML 格式的日志文件。更多细节请参见 第 13 章。
--log-tap
为运行的测试生成 Test Anything Protocol (TAP) 格式的日志文件。更多细节请参见第 13 章。
--log-json
--testdox-html
和 --testdox-text
为运行的测试以 HTML 或纯文本格式生成敏捷文档。更多细节请参见 第 12 章。
--filter
只运行名称与给定模式匹配的测试。若是模式未闭合包裹于分隔符,PHPUnit 将用 /
分隔符对其进行闭合包裹。
测试名称将以如下格式之一进行匹配:
TestNamespace\TestCaseClass::testMethod
默认的测试名称格式等价于在测试方法内使用 __METHOD__
魔术常量。
TestNamespace\TestCaseClass::testMethod with data set #0
当测试拥有数据供给器时,数据的每轮迭代都会将其当前索引附加在默认测试名称结尾处。
TestNamespace\TestCaseClass::testMethod with data set "my named data"
当测试拥有使用命名数据集的数据供给器时,数据的每轮迭代都会将当前名称附加在默认测试名称结尾处。命名数据集的例子参见例 3.1。
例 3.1: 命名数据集
<?php namespace TestNamespace; class TestCaseClass extends \PHPUnit_Framework_TestCase { /** * @dataProvider provider */ public function testMethod($data) { $this->assertTrue($data); } public function provider() { return array( 'my named data' => array(true), 'my data' => array(true) ); } } ?>
/path/to/my/test.phpt
对于 PHPT 测试,其测试名称是文件系统路径。
有效的过滤器模式例子参见例 3.2。
例 3.2: 过滤器模式例子
--filter 'TestNamespace\\TestCaseClass::testMethod'
--filter 'TestNamespace\\TestCaseClass'
--filter TestNamespace
--filter TestCaseClass
--filter testMethod
--filter '/::testMethod .*"my named data"/'
--filter '/::testMethod .*#5$/'
--filter '/::testMethod .*#(5|6|7)$/'
在匹配数据供给器时有一些额外的快捷方式,参见例 3.3。
--testsuite
只运行名称与给定模式匹配的测试套件。
--group
只运行来自指定分组(能够多个)的测试。能够用 @group
标注为测试标记其所属的分组。
@author
标注是 @group
的一个别名,容许按做者来筛选测试。
--exclude-group
排除来自指定分组(能够多个)的测试。能够用 @group
标注为测试标记其所属的分组。
--list-groups
列出全部有效的测试分组。
--test-suffix
只查找文件名以指定后缀(能够多个)结尾的测试文件。
--report-useless-tests
更严格对待事实上不测试任何内容的测试。详情参见 第 6 章。
--strict-coverage
更严格对待意外的代码覆盖。详情参见 第 6 章。
--strict-global-state
更严格对待全局状态篡改。详情参见 第 6 章。
--disallow-test-output
更严格对待测试执行期间产生的输出。详情参见第 6 章。
--disallow-todo-tests
不执行文档注释块中含有 @todo
标注的测试。
--enforce-time-limit
根据测试规模对其加上执行时长限制。详情参见第 6 章。
--process-isolation
每一个测试都在独立的PHP进程中运行。
--no-globals-backup
不要备份并还原 $GLOBALS。更多细节请参见“全局状态”一节。
--static-backup
备份并还原用户定义的类中的静态属性。更多细节请参见“全局状态”一节。
--colors
使用彩色输出。Windows下,用 ANSICON 或 ConEmu。
本选项有三个可能的值:
never
: 彻底不使用彩色输出。当未使用 --colors
选项时,这是默认值。
auto
: 若是当前终端不支持彩色、或者输出被管道输出至其余命令、或输出被重定向至文件时,不使用彩色输出,其他状况使用彩色。
always
: 老是使用彩色输出,即便当前终端不支持彩色、输出被管道输出至其余命令、或输出被重定向至文件。
当使用了 --colors
选项但未指定任何值时,将选择 auto
作为其值。
--columns
定义输出所使用的列数。若是将其值定义为 max
,则使用当前终端所支持的最大列数。
--stderr
选择输出到 STDERR
而非 STDOUT
.
--stop-on-error
首次错误出现后中止执行。
--stop-on-failure
首次错误或失败出现后中止执行。
--stop-on-risky
首次碰到有风险的测试时中止执行。
--stop-on-skipped
首次碰到跳过的测试时中止执行。
--stop-on-incomplete
首次碰到不完整的测试时中止执行。
--verbose
输出更详尽的信息,例如不完整或者跳过的测试的名称。
--debug
输出调试信息,例如当一个测试开始执行时输出其名称。
--loader
指定要使用的 PHPUnit_Runner_TestSuiteLoader
实现。
标准的测试套件加载器将在当前工做目录和 PHP 的 include_path
配置指令中指定的每一个目录内查找源文件。诸如 Project_Package_Class
这样的类名对应的源文件名为 Project/Package/Class.php
。
--repeat
将测试重复运行指定次数。
--tap
使用 Test Anything Protocol (TAP) 报告测试进度。更多细节请参见 第 13 章。
--testdox
将测试进度以敏捷文档方式报告。更多细节请参见 第 12 章。
--printer
指定要使用的结果输出器(printer)。输出器类必须扩展 PHPUnit_Util_Printer
而且实现 PHPUnit_Framework_TestListener
接口。
--bootstrap
在测试前先运行一个 "bootstrap" PHP 文件。
--configuration
,
-c
从 XML 文件中读取配置信息。更多细节请参见附录 C。
若是 phpunit.xml
或 phpunit.xml.dist
(按此顺序)存在于当前工做目录而且未使用 --configuration
,将自动今后文件中读取配置。
--no-configuration
忽略当前工做目录下的 phpunit.xml
与 phpunit.xml.dist
。
--include-path
向 PHP 的 include_path
开头添加指定路径(能够多个)。
-d
设置指定的 PHP 配置选项的值。
请注意,从 4.8 开始,选项不能放在参数以后。
在编写测试时,最费时的部分之一是编写代码来将整个场景设置成某个已知的状态,并在测试结束后将其复原到初始状态。这个已知的状态称为测试的 基境(fixture)。
在例 2.1中,基境十分简单,就是存储在 $stack
变量中的数组。然而,绝大多数时候基境均远比一个简单数组要复杂,用于创建基境的代码量也会随之增加。测试的真正内容就被淹没于创建基境带来的干扰中。当编写多个须要相似基境的测试时这个问题就变得更糟糕了。若是没有来自于测试框架的帮助,就不得不在写每个测试时都将创建基境的代码重复一次。
PHPUnit 支持共享创建基境的代码。在运行某个测试方法前,会调用一个名叫 setUp()
的模板方法。setUp()
是建立测试所用对象的地方。当测试方法运行结束后,无论是成功仍是失败,都会调用另一个名叫 tearDown()
的模板方法。tearDown()
是清理测试所用对象的地方。
在例 2.2中,咱们在测试之间运用生产者-消费者关系来共享基境。这并不是老是预期的方式,甚至有时是不可能的。例 4.1展现了另一个编写测试 StackTest
的方式。在这个方式中,再也不重用基境自己,而是重用创建基境的代码。首先声明一个实例变量,$stack
,用来替代方法内的局部变量。而后把 array
基境的创建放到 setUp()
方法中。最后,从测试方法中去除冗余代码,在 assertEquals()
断言方法中使用新引入的实例变量 $this->stack
替代方法内的局部变量 $stack
。
例 4.1: 用 setUp() 创建栈的基境
<?php class StackTest extends PHPUnit_Framework_TestCase { protected $stack; protected function setUp() { $this->stack = array(); } public function testEmpty() { $this->assertTrue(empty($this->stack)); } public function testPush() { array_push($this->stack, 'foo'); $this->assertEquals('foo', $this->stack[count($this->stack)-1]); $this->assertFalse(empty($this->stack)); } public function testPop() { array_push($this->stack, 'foo'); $this->assertEquals('foo', array_pop($this->stack)); $this->assertTrue(empty($this->stack)); } } ?>
测试类的每一个测试方法都会运行一次 setUp()
和 tearDown()
模板方法(同时,每一个测试方法都是在一个全新的测试类实例上运行的)。
另外,setUpBeforeClass()
与 tearDownAfterClass()
模板方法将分别在测试用例类的第一个测试运行以前和测试用例类的最后一个测试运行以后调用。
例 4.2: 展现全部可用模板方法的例子
<?php class TemplateMethodsTest extends PHPUnit_Framework_TestCase { public static function setUpBeforeClass() { fwrite(STDOUT, __METHOD__ . "\n"); } protected function setUp() { fwrite(STDOUT, __METHOD__ . "\n"); } protected function assertPreConditions() { fwrite(STDOUT, __METHOD__ . "\n"); } public function testOne() { fwrite(STDOUT, __METHOD__ . "\n"); $this->assertTrue(TRUE); } public function testTwo() { fwrite(STDOUT, __METHOD__ . "\n"); $this->assertTrue(FALSE); } protected function assertPostConditions() { fwrite(STDOUT, __METHOD__ . "\n"); } protected function tearDown() { fwrite(STDOUT, __METHOD__ . "\n"); } public static function tearDownAfterClass() { fwrite(STDOUT, __METHOD__ . "\n"); } protected function onNotSuccessfulTest(Exception $e) { fwrite(STDOUT, __METHOD__ . "\n"); throw $e; } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. TemplateMethodsTest::setUpBeforeClass TemplateMethodsTest::setUp TemplateMethodsTest::assertPreConditions TemplateMethodsTest::testOne TemplateMethodsTest::assertPostConditions TemplateMethodsTest::tearDown .TemplateMethodsTest::setUp TemplateMethodsTest::assertPreConditions TemplateMethodsTest::testTwo TemplateMethodsTest::tearDown TemplateMethodsTest::onNotSuccessfulTest FTemplateMethodsTest::tearDownAfterClass Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) TemplateMethodsTest::testTwo Failed asserting that <boolean:false> is true. /home/sb/TemplateMethodsTest.php:30 FAILURES! Tests: 2, Assertions: 2, Failures: 1.phpunit TemplateMethodsTest
理论上说,setUp()
和 tearDown()
是精确对称的,可是实践中并不是如此。实际上,只有在 setUp()
中分配了诸如文件或套接字之类的外部资源时才须要实现 tearDown()
。若是 setUp()
中只建立纯 PHP 对象,一般能够略过 tearDown()
。不过,若是在 setUp()
中建立了大量对象,你可能想要在 tearDown()
中 unset()
指向这些对象的变量,这样它们就能够被垃圾回收机制回收掉。对测试用例对象的垃圾回收动做则是不可预知的。
若是两个基境创建工做略有不一样的测试该怎么办?有两种可能:
若是两个 setUp()
代码仅有微小差别,把有差别的代码内容从 setUp()
移到测试方法内。
若是两个 setUp()
是确实不同,那么须要另一个测试用例类。参考基境创建工做的不一样之处来命名这个类。
有几个好的理由来在测试之间共享基境,可是大部分状况下,在测试之间共享基境的需求都源于某个未解决的设计问题。
一个有实际意义的多测试间共享基境的例子是数据库链接:只登陆数据库一次,而后重用此链接,而不是每一个测试都创建一个新的数据库链接。这样能加快测试的运行。
例 4.3用 setUpBeforeClass()
和 tearDownAfterClass()
模板方法来分别在测试用例类的第一个测试以前和最后一个测试以后链接与断开数据库。
例 4.3: 在同一个测试套件内的不一样测试之间共享基境
<?php class DatabaseTest extends PHPUnit_Framework_TestCase { protected static $dbh; public static function setUpBeforeClass() { self::$dbh = new PDO('sqlite::memory:'); } public static function tearDownAfterClass() { self::$dbh = NULL; } } ?>
须要反复强调的是:在测试之间共享基境会下降测试的价值。潜在的设计问题是对象之间并不是松散耦合。若是解决掉潜在的设计问题并使用桩件(stub)(参见第 9 章)来编写测试,就能达成更好的结果,而不是在测试之间产生运行时依赖并错过改进设计的机会。
使用单件(singleton)的代码很难测试。使用全局变量的代码也同样。一般状况下,欲测代码和全局变量之间会强烈耦合,而且其建立没法控制。另一个问题是,一个测试对全局变量的改变可能会破坏另一个测试。
在 PHP 中,全局变量是这样运做的:
全局变量 $foo = 'bar';
其实是存储为 $GLOBALS['foo'] = 'bar';
的。
$GLOBALS
这个变量是一种被称为超全局变量的变量。
超全局变量是一种在任何变量做用域中都老是可用的内建变量。
在函数或者方法的变量做用域中,要访问全局变量 $foo
,能够直接访问 $GLOBALS['foo']
,或者用 global $foo;
来建立一个引用全局变量的局部变量。
除了全局变量,类的静态属性也是一种全局状态。
默认状况下,PHPUnit 用一种更改全局变量与超全局变量($GLOBALS
、$_ENV
、$_POST
、$_GET
、$_COOKIE
、$_SERVER
、$_FILES
、$_REQUEST
)不会影响到其余测试的方式来运行全部测试。同时,还能够选择将这种隔离扩展到类的静态属性。
对全局变量和类的静态属性的备份与还原操做使用了 serialize()
与 unserialize()
。
某些类的实例对象(好比 PDO
)没法序列化,所以若是把这样一个对象存放在好比说 $GLOBALS
数组内时,备份操做就会出问题。
在“@backupGlobals”一节中所讨论的 @backupGlobals
标注能够用来控制对全局变量的备份与还原操做。另外,还能够提供一个全局变量的黑名单,黑名单中的全局变量将被排除于备份与还原操做以外,就像这样:
class MyTest extends PHPUnit_Framework_TestCase { protected $backupGlobalsBlacklist = array('globalVariable'); // ... }
在方法(例如 setUp()
方法)内对 $backupGlobalsBlacklist
属性进行设置是无效的。
在 “@backupStaticAttributes”一节 中提到的 @backupStaticAttributes
标注能够用于在每一个测试以前备份全部已声明类的静态属性值并在其后恢复。
它所处理的并不仅是测试类自身,而是在测试开始时已声明的全部类。它只做用于静态类属性,不做用于函数内声明的静态变量。
只有启用了 @backupStaticAttributes
的测试方法才会在方法以前执行此操做。若是在此以前运行的某个没有启用 @backupStaticAttributes
的测试方法改变了静态属性的值,那么被备份及还原的将会是这个改变后的值——而非初始声明时提供的默认值。PHP 并不额外记录任何静态变量的声明时提供的初始默认值。
一样的状况也发生于测试内部新加载/声明的类的静态属性上。它们也没法在测试结束以后复原为声明时提供的原始默认值,由于无从得知这些默认值。这些被修改过的值会泄漏到后继测试中。
对单元测试而言,推荐在 setUp()
中显式的重置测试中使用到的静态属性(最好同时在 tearDown()
中执行重置,这样就保证不会影响到后继的测试)。
能够提供黑名单来将静态属性从备份与还原操做中排除出去:
class MyTest extends PHPUnit_Framework_TestCase { protected $backupStaticAttributesBlacklist = array( 'className' => array('attributeName') ); // ... }
在方法(例如 setUp()
)内对 $backupStaticAttributesBlacklist
属性进行设置是无效的。
PHPUnit 的目标之一是测试应当可组合:咱们但愿能将任意数量的测试以任意组合方式运行,例如,整个项目的全部测试,或者项目中的某个组件内的全部类的测试,又或者仅仅某单个类的测试。
PHPUnit 支持好几种不一样的方式来组织测试以及将它们编排组合成测试套件。本章介绍了最经常使用的方法。
编排测试套件的各类方式中,最简单的大概就是把全部测试用例源文件放在一个测试目录中。经过对测试目录进行递归遍历,PHPUnit 能自动发现并运行测试。
如今来看看 sebastianbergmann/money 这个库的测试套件。在这个项目的目录结构中,能够看到 tests
目录下的测试用例类镜像了 src
目录下被测系统(SUT, System Under Test)的包(package)与类(class)的结构:
src tests `-- Currency.php `-- CurrencyTest.php `-- IntlFormatter.php `-- IntlFormatterTest.php `-- Money.php `-- MoneyTest.php `-- autoload.php
要运行这个库的所有测试,只要将 PHPUnit 命令行测试执行器指向测试目录便可:
PHPUnit 5.0.0 by Sebastian Bergmann. ................................. Time: 636 ms, Memory: 3.50Mb OK (33 tests, 52 assertions)phpunit --bootstrap src/autoload.php tests
当 PHPUnit 命令行测试执行器指向一个目录时,它会在目录下查找 *Test.php
文件。
若是只想运行在 CurrencyTest
文件中的 tests/CurrencyTest.php
测试用例类中声明的测试,能够使用以下命令:
PHPUnit 5.0.0 by Sebastian Bergmann. ........ Time: 280 ms, Memory: 2.75Mb OK (8 tests, 8 assertions)phpunit --bootstrap src/autoload.php tests/CurrencyTest
若是想要对运行哪些测试有更细粒度的控制,能够使用 --filter
选项:
PHPUnit 5.0.0 by Sebastian Bergmann. .. Time: 167 ms, Memory: 3.00Mb OK (2 test, 2 assertions)phpunit --bootstrap src/autoload.php --filter testObjectCanBeConstructedForValidConstructorArgument tests
这种方法的缺点是没法控制测试的运行顺序。这可能致使测试的依赖关系方面的问题,参见 “测试的依赖关系”一节。在下一节中,能够看到如何用 XML 配置文件来明确指定测试的执行顺序。
PHPUnit的 XML 配置文件(附录 C)也能够用于编排测试套件。例 5.1展现了一个最小化的 phpunit.xml
例子,它将在递归遍历 tests
时添加全部在 *Test.php
文件中找到的 *Test
类。
例 5.1: 用 XML 配置来编排测试套件
<phpunit bootstrap="src/autoload.php"> <testsuites> <testsuite name="money"> <directory>tests</directory> </testsuite> </testsuites> </phpunit>
若是 phpunit.xml
或 phpunit.xml.dist
(按此顺序)存在于当前工做目录而且未使用 --configuration
,将自动今后文件中读取配置。
能够明确指定测试的执行顺序:
在执行测试时,PHPUnit 能够进行一些额外的检查,见下文。
PHPUnit 能够更严格对待事实上不测试任何内容的测试。此项检查能够用命令行选项 --report-useless-tests
或在 PHPUnit 的 XML 配置文件中设置 beStrictAboutTestsThatDoNotTestAnything="true"
来启用。
在启用本项检查后,若是某个测试未进行任何断言,它将被标记为有风险。仿件对象中的预期和诸如 @expectedException
这样的标注一样视为断言。
PHPUnit 能够更严格对待意外的代码覆盖。此项检查能够用命令行选项 --strict-coverage
或在 PHPUnit 的 XML 配置文件中设置 checkForUnintentionallyCoveredCode="true"
来启用。
在启用本项检查后,若是某个带有 @covers
标注的测试执行了未在 @covers
或 @uses
标注中列出的代码,它将被标记为有风险。
PHPUnit 能够更严格对待测试执行期间产生的输出。 此项检查能够用命令行选项 --disallow-test-output
或在 PHPUnit 的 XML 配置文件中设置 beStrictAboutOutputDuringTests="true"
来启用。
在启用本项检查后,若是某个测试产生了输出,例如,在测试代码或被测代码中调用了 print
,它将被标记为有风险。
若是安装了 PHP_Invoker
包而且 pcntl
扩展可用,那么能够对测试的执行时长进行限制。此时间限制能够用命令行选项 --enforce-time-limit
或在 PHPUnit 的 XML 配置文件中设置 beStrictAboutTestSize="true"
来启用。
带有 @large
标注的测试若是执行时间超过60秒将视为失败。此超时限制能够经过XML配置文件中的 timeoutForLargeTests
属性进行配置。
带有 @medium
标注的测试若是执行时间超过10秒将视为失败。此超时限制能够经过XML配置文件中的 timeoutForMediumTests
属性进行配置。
没有 @medium
或 @large
标注的测试都将视同为带有 @small
标注,这类测试若是执行时间超过1秒将视为失败。此超时限制能够经过XML配置文件中的 timeoutForSmallTests
属性进行配置。
开始写新的测试用例类时,可能想从写下空测试方法开始,好比:
public function testSomething() { }
以此来跟踪须要编写的测试。空测试的问题是 PHPUnit 框架会将它们解读为成功。这种错误解读致使错误报告变得毫无用处——没法分辨出测试是真的成功了仍是根本就未编写实现。在未实现的测试中调用 $this->fail()
一样没啥帮助,由于测试将被解读为失败。这和将未实现的测试解读为成功是同样的错误。
假如把成功的测试视为绿灯、测试失败视为红灯,那么还额外须要黄灯来将测试标记为未完成或还没有实现。PHPUnit_Framework_IncompleteTest
是一个标记接口,用于将测试方法抛出的异常标记为测试未完成或目前还没有实现而致使的结果。PHPUnit_Framework_IncompleteTestError
是这个界面的标准实现。
例 7.1展现了一个测试用例类 SampleTest
,它有一个测试方法 testSomething()
。经过在测试方法中调用便捷方法 markTestIncomplete()
(会自动抛出一个 PHPUnit_Framework_IncompleteTestError
异常)将这个测试标记为未完成。
例 7.1: 将测试标记为未完成
<?php class SampleTest extends PHPUnit_Framework_TestCase { public function testSomething() { // 可选:若是愿意,在这里随便测试点什么。 $this->assertTrue(TRUE, '这应该已是能正常工做的。'); // 在这里中止,并将此测试标记为未完成。 $this->markTestIncomplete( '此测试目前还没有实现。' ); } } ?>
在 PHPUnit 命令行测试执行器的输出中,未完成的测试记为 I
,以下例所示:
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. I Time: 0 seconds, Memory: 3.95Mb There was 1 incomplete test: 1) SampleTest::testSomething This test has not been implemented yet. /home/sb/SampleTest.php:12 OK, but incomplete or skipped tests! Tests: 1, Assertions: 1, Incomplete: 1.phpunit --verbose SampleTest
表 7.1列举了用于将测试标记为未完成的 API。
并不是全部测试都能在任何环境中运行。好比说,考虑这样一种状况:一个数据库抽象层,针对其所支持的各类数据库系统有多个不一样的驱动程序。针对 MySQL 驱动程序的测试固然只在 MySQL 服务器可用才能运行。
例 7.2 展现了一个测试用例类 DatabaseTest
,它有一个测试方法 testConnection()
。在测试用例类的 setUp()
模板方法中,检查了 MySQLi 扩展是否可用,而且在扩展不可用时用 markTestSkipped()
方法来跳过此测试。
例 7.2: 跳过某个测试
<?php class DatabaseTest extends PHPUnit_Framework_TestCase { protected function setUp() { if (!extension_loaded('mysqli')) { $this->markTestSkipped( 'The MySQLi 扩展不可用。' ); } } public function testConnection() { // ... } } ?>
在 PHPUnit 命令行测试执行器的输出中,被跳过的测试记为 S
,以下例所示:
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. S Time: 0 seconds, Memory: 3.95Mb There was 1 skipped test: 1) DatabaseTest::testConnection The MySQLi extension is not available. /home/sb/DatabaseTest.php:9 OK, but incomplete or skipped tests! Tests: 1, Assertions: 0, Skipped: 1.phpunit --verbose DatabaseTest
表 7.2列举了用于跳过测试的 API。
除了上述方法,还能够用 @requires
标注来表达测试用例的一些常见前提条件。
表 7.3. 可能的 @requires 用法
类型 | 可能的值 | 范例 | 其余范例 |
---|---|---|---|
PHP |
任何 PHP 版本标识符 | @requires PHP 5.3.3 | @requires PHP 5.4-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 curl |
例 7.3: 用 @requires 来跳过测试
<?php /** * @requires extension mysqli */ class DatabaseTest extends PHPUnit_Framework_TestCase { /** * @requires PHP 5.3 */ public function testConnection() { // 测试要求有 mysqli 扩展,而且 PHP >= 5.3 } // ... 全部其余要求有 mysqli 扩展的测试 } ?>
若是使用了某种在特定版本的 PHP 下没法编译的语法,请在此章节内查找 XML 配置信息中关于版本依赖的信息:“测试套件”一节
在各类编程语言中,许多入门与中级的单元测试范例都暗示着这样一种信息:很容易用简单的测试来对应用程序的逻辑进行测试。可是对于以数据库为中心的应用程序而言,这与现实相去甚远。一旦开始使用诸如 Wordpress、TYPO三、或 Symfony(配合 Doctrine 或 Propel)之类的东西,就很容易在用 PHPUnit 时碰到超多问题:正是因为这些库和数据库之间实在耦合的太紧密了。
请确保已经安装了 PHP 扩展模块 pdo
和与数据库对应的特定扩展,好比 pdo_mysql
。不然如下范例是没法运行的。
你大概会在平常工做面对的项目中经历这一幕。你打算把你那或生疏或纯熟的 PHPUnit 技能用到工做中去,结果被如下问题之一卡住了:
待测方法执行了一个至关大的 JOIN 操做,而且获得的数据用于计算某些重要的结果。
业务逻辑中混合执行了 SELECT、INSERT、UPDATE 和 DELETE 语句。
为了给待测方法创建合理的初始数据,须要在两个以上(可能远超过)表里设置测试数据。
DbUnit 扩展大大简化了为测试设置数据库的操做,而且能够在对数据执行了一系列操做以后验证数据库的内容。
DbUnit 目前支持 MySQL、PostgreSQL、Oracle 和 SQLite。经过集成 Zend Framework 或 Doctrine 2,也能够访问其余数据库系统,好比 IBM DB2 或者 Microsoft SQL Server。
为何全部单元测试的范例都不包含数据库交互?这里有个很好的理由:这类测试的创建和维护都很复杂。对数据库进行测试时,须要考虑如下这些变数:
数据库和表
向表中插入测试所须要的行
测试运行完毕后验证数据库的状态
每一个新测试都要清理数据库
许多数据库 API,好比 PDO、MySQLi 或者 OCI8,都十分繁琐且书写起来十分冗长,所以,手工进行这些步骤绝对是噩梦。
测试代码应当尽量简短精确,这有若干缘由:
你不但愿由于生产代码的小变动而须要对测试代码进行数量可观的修改。
你但愿在哪怕好几个月之后也能轻松地阅读并理解测试代码。
另外,必须认识到,对于代码而言,本质上来讲数据库是全局输入变量。测试套件中的两个不一样的测试多是运行在同一个数据库上的,而且可能把数据重用好屡次。一个测试中出现的失败很容易影响到后继测试的结果,从而让整个测试过程变得很是艰难。前面提到的清理步骤对于解决“数据库是全局输入”的问题是很是重要的。
DbUnit 以一种优雅的方式来帮助简化数据库测试中的全部这些问题。
PHPUnit 没法帮你解决的问题是,相对于不使用数据的测试而言,数据库测试是很是慢的。随着数据库交互规模的增大,运行测试可能须要耗费可观的时间。然而,只要保持每一个测试所使用的数据量较小而且尽量用非数据库测试来对代码进行测试,即便很大的测试套件也能轻松在一分钟内跑完。
以 Doctrine 2 为例,此项目的测试套件目前包含了大约1000个测试,其中将近一半访问了数据库。可是在一台安装了MySQL的普通的台式机上,整个测试套件依然能在15秒钟内跑完。
Gerard Meszaros 在他的书《xUnit 测试模式》中列出了单元测试的四个阶段:
创建基境(fixture)
执行被测系统
验证结果
拆除基境(fixture)
什么是基境(fixture)?
基境(fixture)是对开始执行某个测试时应用程序和数据库所处初始状态的描述。
对数据库进行测试至少要处理创建与拆除的步骤,在其中完成清理工做,并将所需的基境数据写入表内。于是,对于数据库扩展模块而言,在数据库测试中有很好的理由将这四个步骤还原成相似下面这样的工做流程,这个流程对于每一个测试都会完整执行:
通常而言,使用 PHPUnit 时,测试用例都是按以下方式扩展自 PHPUnit_Framework_TestCase
类:
<?php class MyTest extends PHPUnit_Framework_TestCase { public function testCalculate() { $this->assertEquals(2, 1 + 1); } } ?>
若是测试代码用到了数据库扩展模块,那么创建的过程就会更复杂一些,须要扩展另外一个抽象 TestCase 类,它要求实现两个抽象方法,getConnection()
和 getDataSet()
:
<?php class MyGuestbookTest extends PHPUnit_Extensions_Database_TestCase { /** * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection */ public function getConnection() { $pdo = new PDO('sqlite::memory:'); return $this->createDefaultDBConnection($pdo, ':memory:'); } /** * @return PHPUnit_Extensions_Database_DataSet_IDataSet */ public function getDataSet() { return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/guestbook-seed.xml'); } } ?>
为了让清理与载入基境的功能正常运做,PHPUnit 数据库扩展模块须要用 PDO 库来实现跨供应商抽象访问数据库链接。重要的是要注意到,使用 PHPUnit 的数据库扩展模块并不要求应用程序自己基于PDO,PDO链接仅仅用于清理和创建基境。
在以前的例子里,咱们在内存中建立 Sqlite 数据库并创建了链接,将此链接传递给 createDefaultDBConnection
方法,这个方法将 PDO 实例和第二参数(数据库名)包装在一个很是简单的数据库链接抽象层中,这个抽象层的类型是 PHPUnit_Extensions_Database_DB_IDatabaseConnection
。
“使用数据库链接”一节解说了这个接口的API以及如何充分利用它们。
getDataSet()
方法定义了在每一个测试执行以前的数据库初始状态应该是什么样。数据库的状态经过由 PHPUnit_Extensions_Database_DataSet_IDataSet
所表明的 DataSet(数据集)和由 PHPUnit_Extensions_Database_DataSet_IDataTable
所表明的 DataTable(数据表)这两个概念进行抽象。下一节将详细讲述这些概念是如何运做的以及在数据库测试中使用它们有什么好处。
对于具体实现,只须要知道 setUp()
中会调用一次 getDataSet()
方法来接收基境数据集并将其插入数据库。在范例中使用了工厂方法 createFlatXMLDataSet($filename)
,它表明一个用 XML 表示的数据集。
PHPUnit 假设在测试运行以前数据库以及其中的全部表(table)、触发器(trigger)、序列(Sequence)和视图(view)都已经建立好。这意味着开发者必须在运行测试套件以前确保数据库已经正确创建。
有几种方法来达成这个数据库测试的先决条件。
若是使用的是持久化数据库(不是 Sqlite Memory),能够很轻松地用 phpMyAdmin(针对MySQL)之类的工具来一次性创建数据库,并在每一个测试中复用这个数据库。
若是使用的是诸如 Doctrine 2 或 Propel 这样的库,能够用它们的API来在测试运行前一次性创建所需的数据库。能够利用 PHPUnit 的引导和配置 功能来在每次测试运行时执行这些代码。
从前面的实现范例中容易发现 getConnection()
方法是至关稳定的,能够在不一样的数据库测试用例中重用。另外,为了保持测试的性能良好和数据库的开销较低,能够对代码进行一点重构,来为应用程序造成一个通用的抽象测试用例,同时依然能够为每一个具体测试用例指定不一样的数据基境:
<?php abstract class MyApp_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase { // 只实例化 pdo 一次,供测试的清理和装载基境使用 static private $pdo = null; // 对于每一个测试,只实例化 PHPUnit_Extensions_Database_DB_IDatabaseConnection 一次 private $conn = null; final public function getConnection() { if ($this->conn === null) { if (self::$pdo == null) { self::$pdo = new PDO('sqlite::memory:'); } $this->conn = $this->createDefaultDBConnection(self::$pdo, ':memory:'); } return $this->conn; } } ?>
这个例子里,数据库链接信息硬编码在 PDO 链接里了。PHPUnit 有另一个绝妙的特性,可让这个 TestCase 类更加通用。经过 XML 配置 能够为每一个测试单独配置数据库链接信息。首先,在应用程序的 tests/ 目录下建立 “phpunit.xml” 文件,内容大致是这样:
<?xml version="1.0" encoding="UTF-8" ?> <phpunit> <php> <var name="DB_DSN" value="mysql:dbname=myguestbook;host=localhost" /> <var name="DB_USER" value="user" /> <var name="DB_PASSWD" value="passwd" /> <var name="DB_DBNAME" value="myguestbook" /> </php> </phpunit>
如今能够修改 TestCase 类了,像这样:
<?php abstract class Generic_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase { // 只实例化 pdo 一次,供测试的清理和装载基境使用 static private $pdo = null; // 对于每一个测试,只实例化 PHPUnit_Extensions_Database_DB_IDatabaseConnection 一次 private $conn = null; final public function getConnection() { if ($this->conn === null) { if (self::$pdo == null) { self::$pdo = new PDO( $GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD'] ); } $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']); } return $this->conn; } } ?>
如今能够从命令行界面以不一样的配置来运行数据库测试套件了:
user@desktop> phpunit --configuration developer-a.xml MyTests/user@desktop> phpunit --configuration developer-b.xml MyTests/
在开发机上进行开发时可以轻松的针对不一样的目标数据库来运行数据库测试显得很是重要。若是多个开发人员在同一个数据库链接上运行数据库测试,很容易由于竞态而致使测试失败。
PHPUnit 的数据库扩展模块的核心概念是 DataSet(数据集)和 DataTable(数据表)。为了掌握如何使用 PHPUnit 进行测试,须要试着去了解这些简单的概念。DataSet(数据集)和 DataTable(数据表)是围绕着数据库表、行、列的抽象层。经过一套简单的API,底层数据库内容被隐藏在对象结构之下,同时,这个对象结构也能够用其余非数据库数据源来实现。
为了能比较实际内容和预期内容,这种抽象是必须的。预期内容能够用诸如 XML、 YAML、 CSV 文件或者 PHP 数组等方式来表达。DataSet 和 DataTable 接口以语义类似的方式模拟关系数据库存储,从而可以对这些概念上彻底不一样的数据源进行比较。
在测试中,数据库断言的工做流由如下三个简单的步骤组成:
用表名称来指定数据库中的一个或多个表(其实是指定了一个数据集)
用你喜欢的格式(YAML、XML等等)来指定预期数据集
断言这两个数据集陈述是彼此相等的。
在 PHPUnit 的数据库扩展中,断言并不是惟一使用 DataSet 和 DataTable 的情形。就像上一节中所展现的那样,它们也用于描述数据库的初始内容。数据库 TestCase 类强制要求定义一个基境数据集,随后用它来:
根据此数据集所指定的全部表名,将数据库中对应表内的行所有删除。
将数据集内数据表中的全部行写入数据库。
有三种不一样类型的 DataSet/DataTable:
基于文件的 DataSet 和 DataTable
基于查询的 DataSet 和 DataTable
筛选与组合 DataSet 和 DataTable
基于文件的数据集和表通常用于初始化基境或描述数据库的预期状态。
最多见的一种数据集名叫 Flat XML。这是一种很是简单的 XML 格式,根节点为 <dataset>
,根节点下的每一个标签就表明数据库中的一行数据。标签的名称就等于表名,而每一个属性表明一个列。一个简单的留言本应用程序的例子大体上多是这样:
<?xml version="1.0" ?> <dataset> <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" /> <guestbook id="2" content="I like it!" user="nancy" created="2010-04-26 12:14:20" /> </dataset>
显然,这很是易于编写。在这里,<guestbook>
是表名,这个表内有两行记录,每行有四个列:“id”、“content”、“user” 和 “created”,以及各自的值。
不过,这种简单性是有代价的。
从上面这个例子里不太容易看出该如何指定一个空表。其实能够插入一个没有属性值的标签,以空表的名字做为标签名。空的 guestbook 表所对应的 Flat XML 文件大体上多是这样:
<?xml version="1.0" ?> <dataset> <guestbook /> </dataset>
在 Flat XML DataSet 中,要处理 NULL 值会很是烦。在几乎全部数据库中(Oracle 是个例外),NULL 值和空字符串值是有区别的,这一点在 Flat XML 格式中很难表述。能够在数据行的表述中省略掉对应的属性来表示NULL值。假定上面这个留言本经过在 user 列使用 NULL 值的方式来容许匿名留言,那么 guestbook 表的内容多是这样:
<?xml version="1.0" ?> <dataset> <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" /> <guestbook id="2" content="I like it!" created="2010-04-26 12:14:20" /> </dataset>
在这个例子里第二个条目是匿名发表的。可是这为列的识别带来了一个很是严重的问题。在数据集相等断言的断定过程当中,每一个数据集都须要指明每一个表拥有哪些列。若是有一个列在数据表的全部行里其值都是 NULL,那么数据库扩展模块又该从何得知表中包含这个列呢?
在这里,Flat XML DataSet 作了一个关键假设:一个表的列信息由此表第一行的属性定义决定。在上面这个例子里,这意味着 guestbook 有 “id”、“content”、“user” 和 “created” 这几个列。第二行中 “user” 列没有定义,所以将向数据库中插入 NULL 值。
若是从数据集中删掉第一行,由于没有指定 “user”,guestbook 表拥有的列就只剩下 “id”、“content” 和 “created”。
要在有 NULL 值的状况下有效地使用 Flat XML Dataset,就必须保证每一个表的第一行不包含 NULL 值,只有后继的那些行才能省略属性。这就有点棘手,由于数据行的排列顺序也是数据断言的一个相关因素。
反过来,若是在 Flat XML Dataset 中只指明了实际表中全部列的某个子集,那么全部省略掉的列都会设为它们的的默认值。若是某个省略掉的列的定义是 “NOT NULL DEFAULT NULL”,就会出现错误。
总的来讲,建议只在不须要 NULL 值的状况下使用 Flat XML Dataset。
能够在数据库 TestCase 中调用 createFlatXmlDataSet($filename)
方法来建立 Flat XML Dataset 实例:
<?php class MyTestCase extends PHPUnit_Extensions_Database_TestCase { public function getDataSet() { return $this->createFlatXmlDataSet('myFlatXmlFixture.xml'); } } ?>
有另一种更加结构化的 XML DataSet,它写起来有点冗长,可是规避了 Flat XML DataSet 所存在的 NULL 问题。在根节点 <dataset>
内,能够指定 <table>
、<column>
、 <row>
、<value>
和 <null />
标签。和上面用 Flat XML 所定义的留言本数据集等价的 XML DataSet 以下:
<?xml version="1.0" ?> <dataset> <table name="guestbook"> <column>id</column> <column>content</column> <column>user</column> <column>created</column> <row> <value>1</value> <value>Hello buddy!</value> <value>joe</value> <value>2010-04-24 17:15:23</value> </row> <row> <value>2</value> <value>I like it!</value> <null /> <value>2010-04-26 12:14:20</value> </row> </table> </dataset>
所定义的每一个 <table>
都有一个名称,而且必须有对全部列及其名称的定义。其下能够包含零个或任意正整数个 <row>
元素。没有定义 <row>
意味着这是个空表。<value>
和 <null />
标签必须按照以前给定 <column>
元素的顺序来指定。<null />
标签显然意味着这个值为 NULL。
能够在数据库 TestCase 中调用 createXmlDataSet($filename)
方法来建立 XML DataSet 实例:
<?php class MyTestCase extends PHPUnit_Extensions_Database_TestCase { public function getDataSet() { return $this->createXMLDataSet('myXmlFixture.xml'); } } ?>
这种新的 XML 格式是 MySQL 数据库服务器专用的。PHPUnit 3.5 加入了对这种格式的支持。能够用 mysqldump
工具来生成这种格式的文件。与一样为 mysqldump
所支持的 CSV 数据集不一样,这种 XML 格式能够在单个文件中包含多个表的数据。要生成这种格式的文件,能够这样调用 mysqldump
:
mysqldump --xml -t -u [username] --password=[password] [database] > /path/to/file.xml
能够在数据库 TestCase 中调用 createMySQLXMLDataSet($filename)
方法来使用这个文件:
<?php class MyTestCase extends PHPUnit_Extensions_Database_TestCase { public function getDataSet() { return $this->createMySQLXMLDataSet('/path/to/file.xml'); } } ?>
也能够用 YAML DataSet 来写这个留言本的例子:
guestbook: - id: 1 content: "Hello buddy!" user: "joe" created: 2010-04-24 17:15:23 - id: 2 content: "I like it!" user: created: 2010-04-26 12:14:20
简单方便,同时还解决了和它相似的 FLat XML DataSet 所具备的 NULL 问题。在 YAML 中,只有列名而没有指定值就表示 NULL。空白字符串则这样指定:column1: ""
。
目前,数据库 TestCase 中没有 YAML DataSet 的工厂方法,所以须要手工进行实例化:
<?php class YamlGuestbookTest extends PHPUnit_Extensions_Database_TestCase { protected function getDataSet() { return new PHPUnit_Extensions_Database_DataSet_YamlDataSet( dirname(__FILE__)."/_files/guestbook.yml" ); } } ?>
另一种基于文件的 DataSet 是基于 CSV 文件的。数据集中的每一个表用一个单独的 CSV 文件表示。对于留言本的例子,能够这样定义 guestbook-table.csv 文件:
id,content,user,created 1,"Hello buddy!","joe","2010-04-24 17:15:23" 2,"I like it!","nancy","2010-04-26 12:14:20"
用 Excel 或者 OpenOffice 来对这种格式进行编辑是很是方便的,可是在 CSV DataSet 中没法指定 NULL 值。给出一个空白列的结果是往这个列中插入数据库的默认空值。
能够这样建立 CSV DataSet:
<?php class CsvGuestbookTest extends PHPUnit_Extensions_Database_TestCase { protected function getDataSet() { $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet(); $dataSet->addTable('guestbook', dirname(__FILE__)."/_files/guestbook.csv"); return $dataSet; } } ?>
在 PHPUnit 的数据库扩展中,(尚)没有基于数组的 DataSet,不过很容易自行实现之。留言本的例子大体是这样:
<?php class ArrayGuestbookTest extends PHPUnit_Extensions_Database_TestCase { protected function getDataSet() { return new MyApp_DbUnit_ArrayDataSet(array( 'guestbook' => array( array('id' => 1, 'content' => 'Hello buddy!', 'user' => 'joe', 'created' => '2010-04-24 17:15:23'), array('id' => 2, 'content' => 'I like it!', 'user' => null, 'created' => '2010-04-26 12:14:20'), ), )); } } ?>
PHP 版本的 DataSet 相比于全部其余基于文件的 DataSet 相比有很明显的优势:
PHP 数组显然能够处理 NULL
值。
不须要为断言提供任何额外文件,能够直接在 TestCase 中指定。
对于这种 DataSet 而言,和平直 XML、CSV、YAML DataSet 同样,表的列名信息由第一个指定的行的键名定义。在上面这个例子里,就是 “id”、“content”、“user” 和 “created”。
这个数组 DataSet 类的实现是很是简单直接的:
<?php class MyApp_DbUnit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet { /** * @var array */ protected $tables = array(); /** * @param array $data */ public function __construct(array $data) { foreach ($data AS $tableName => $rows) { $columns = array(); if (isset($rows[0])) { $columns = array_keys($rows[0]); } $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns); $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData); foreach ($rows AS $row) { $table->addRow($row); } $this->tables[$tableName] = $table; } } protected function createIterator($reverse = FALSE) { return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse); } public function getTable($tableName) { if (!isset($this->tables[$tableName])) { throw new InvalidArgumentException("$tableName is not a table in the current database."); } return $this->tables[$tableName]; } } ?>
对于数据库断言,不只须要有基于文件的 DataSet,同时也须要有一种内含数据库实际内容的基于查询/SQL 的 DataSet。Query DataSet 在此闪亮登场:
<?php $ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection()); $ds->addTable('guestbook'); ?>
单纯以名称来添加表是一种隐式地用如下查询来定义 DataTable 的方法:
<?php $ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection()); $ds->addTable('guestbook', 'SELECT * FROM guestbook'); ?>
能够在这种用法中为你的表任意指定查询,例如限定行、列,或者加上 ORDER BY
子句:
<?php $ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection()); $ds->addTable('guestbook', 'SELECT id, content FROM guestbook ORDER BY created DESC'); ?>
在关于数据库断言的那一节中有更多关于如何使用 Query DataSet 的细节。
经过访问测试所使用的数据库链接,能够自动建立包含数据库全部表以及其内容的 DataSet。所使用的数据库由数据库链接工厂方法的第二个参数指定。
能够像 testGuestbook()
中那样建立整个数据库所对应的 DataSet,或者像 testFilteredGuestbook()
方法中那样用一个白名单来将 DataSet 限制在若干表名的集合上。
<?php class MySqlGuestbookTest extends PHPUnit_Extensions_Database_TestCase { /** * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection */ public function getConnection() { $database = 'my_database'; $user = 'my_user'; $password = 'my_password'; $pdo = new PDO('mysql:...', $user, $password); return $this->createDefaultDBConnection($pdo, $database); } public function testGuestbook() { $dataSet = $this->getConnection()->createDataSet(); // ... } public function testFilteredGuestbook() { $tableNames = array('guestbook'); $dataSet = $this->getConnection()->createDataSet($tableNames); // ... } } ?>
前面谈到了 Flat XML 和 CSV DataSet 所存在的 NULL 问题,不过有一种稍微有点复杂的解决方法可让这两种数据集都能正常处理 NULL。
Replacement DataSet 是已有数据集的修饰器(decorator),可以将数据集中任意列的值替换为其余替代值。为了让留言本的例子可以处理 NULL 值,首先指定相似这样的文件:
<?xml version="1.0" ?> <dataset> <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" /> <guestbook id="2" content="I like it!" user="##NULL##" created="2010-04-26 12:14:20" /> </dataset>
而后将 Flat XML DataSet 包装在 Replacement DataSet 中:
<?php class ReplacementTest extends PHPUnit_Extensions_Database_TestCase { public function getDataSet() { $ds = $this->createFlatXmlDataSet('myFlatXmlFixture.xml'); $rds = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($ds); $rds->addFullReplacement('##NULL##', null); return $rds; } } ?>
若是有一个很是大的基境文件,能够用数据集筛选器来为须要包含在子数据集中的表和列指定白/黑名单。与 DB DataSet 联用来对数据集中的列进行筛选尤为方便。
<?php class DataSetFilterTest extends PHPUnit_Extensions_Database_TestCase { public function testIncludeFilteredGuestbook() { $tableNames = array('guestbook'); $dataSet = $this->getConnection()->createDataSet(); $filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet); $filterDataSet->addIncludeTables(array('guestbook')); $filterDataSet->setIncludeColumnsForTable('guestbook', array('id', 'content')); // .. } public function testExcludeFilteredGuestbook() { $tableNames = array('guestbook'); $dataSet = $this->getConnection()->createDataSet(); $filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet); $filterDataSet->addExcludeTables(array('foo', 'bar', 'baz')); // only keep the guestbook table! $filterDataSet->setExcludeColumnsForTable('guestbook', array('user', 'created')); // .. } } ?>
注意:不能对同一个表同时应用排除与包含两种列筛选器,只能分别应用于不一样的表。另外,表的白名单和黑名单也只能选择其一,不能两者同时使用。
Composite DataSet 能将多个已存在的数据集聚合成单个数据集,所以很是有用。若是多个数据集中存在一样的表,其中的数据行将按照指定的顺序进行追加。例如,假设有两个数据集, fixture1.xml:
<?xml version="1.0" ?> <dataset> <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" /> </dataset>
和 fixture2.xml:
<?xml version="1.0" ?> <dataset> <guestbook id="2" content="I like it!" user="##NULL##" created="2010-04-26 12:14:20" /> </dataset>
经过 Composite DataSet 能够把这两个基境文件聚合在一块儿:
<?php class CompositeTest extends PHPUnit_Extensions_Database_TestCase { public function getDataSet() { $ds1 = $this->createFlatXmlDataSet('fixture1.xml'); $ds2 = $this->createFlatXmlDataSet('fixture2.xml'); $compositeDs = new PHPUnit_Extensions_Database_DataSet_CompositeDataSet(); $compositeDs->addDataSet($ds1); $compositeDs->addDataSet($ds2); return $compositeDs; } } ?>
为了理解 DataSet 和 DataTable 的内部实现,让咱们来看看 DataSet 的接口。若是没打算自行实现 DataSet 或者 DataTable,能够直接跳过这一部分。
<?php interface PHPUnit_Extensions_Database_DataSet_IDataSet extends IteratorAggregate { public function getTableNames(); public function getTableMetaData($tableName); public function getTable($tableName); public function assertEquals(PHPUnit_Extensions_Database_DataSet_IDataSet $other); public function getReverseIterator(); } ?>
这些 public 接口在数据库 TestCase 中 assertDataSetsEqual()
断言内使用,用以检测数据集是否相等。IDataSet 中继承自 IteratorAggregate
接口的 getIterator()
方法用于对数据集中的全部表进行迭代。逆序迭代器让 PHPUnit 可以按照与建立时相反的顺序对全部表执行 TRUNCATE 操做,以此来保证知足外键约束。
根据具体实现的不一样,要采起不一样的方法来将表实例添加到数据集中。例如,在全部基于文件的数据集中,表都是在构造过程当中直接从源文件生成并加入数据集中,好比 YamlDataSet
、XmlDataSet
和 FlatXmlDataSet
均是如此。
数据表则由如下接口表示:
<?php interface PHPUnit_Extensions_Database_DataSet_ITable { public function getTableMetaData(); public function getRowCount(); public function getValue($row, $column); public function getRow($row); public function assertEquals(PHPUnit_Extensions_Database_DataSet_ITable $other); } ?>
除了 getTableMetaData()
方法以外,这个接口是一目了然的。数据库扩展模块中的各类断言(将于下一章中介绍)用到了全部这些方法,所以它们所有都是必需的。getTableMetaData()
方法须要返回一个实现了 PHPUnit_Extensions_Database_DataSet_ITableMetaData
接口的描述表结构的对象。这个对象包含以下信息:
表的名称
表的列名数组,按照列在结果集中出现的顺序排列。
构成主键的列的数组。
这个接口还包含有检验两个表的元数据实例是否彼此相等的断言,供数据集相等断言使用。
由数据库 TestCase 中的 getConnection()
方法所返回的链接界面有三个颇有意思的方法:
<?php interface PHPUnit_Extensions_Database_DB_IDatabaseConnection { public function createDataSet(Array $tableNames = NULL); public function createQueryTable($resultName, $sql); public function getRowCount($tableName, $whereClause = NULL); // ... } ?>
createDataSet()
方法建立一个在数据集实现一节描述过的 Database (DB) DataSet(数据库数据集)。
<?php class ConnectionTest extends PHPUnit_Extensions_Database_TestCase { public function testCreateDataSet() { $tableNames = array('guestbook'); $dataSet = $this->getConnection()->createDataSet(); } } ?>
createQueryTable()
方法用于建立 QueryTable 的实例,须要为其指定结果名称和所使用的 SQL 查询。当涉及到结果/表的断言(如后面关于数据库断言 API 那一节所示)时,这个方法会很方便。
<?php class ConnectionTest extends PHPUnit_Extensions_Database_TestCase { public function testCreateQueryTable() { $tableNames = array('guestbook'); $queryTable = $this->getConnection()->createQueryTable('guestbook', 'SELECT * FROM guestbook'); } } ?>
getRowCount()
方法提供了一种方便的方式来取得表中的行数,而且还能够选择附加一个 WHERE 子句来在计数前对数据行进行过滤。它能够和一个简单的相等断言合用:
<?php class ConnectionTest extends PHPUnit_Extensions_Database_TestCase { public function testGetRowCount() { $this->assertEquals(2, $this->getConnection()->getRowCount('guestbook')); } } ?>
做为测试工具,数据库扩展模块理所固然会提供一些断言,能够用来验证数据库的当前状态、表的当前状态、表中数据行的数量。本节将详细描述这部分功能:
不少时候,确认表中是否包含特定数量的数据行是很是有帮助的。能够轻松作到这一点,不须要任何额外的使用链接 API 的粘合剂代码。好比说,在往留言本中插入一个新行以后,想要确认在表中除了以前的例子中一直都有的两行以外还有第三行:
<?php class GuestbookTest extends PHPUnit_Extensions_Database_TestCase { public function testAddEntry() { $this->assertEquals(2, $this->getConnection()->getRowCount('guestbook'), "Pre-Condition"); $guestbook = new Guestbook(); $guestbook->addEntry("suzy", "Hello world!"); $this->assertEquals(3, $this->getConnection()->getRowCount('guestbook'), "Inserting failed"); } } ?>
前面的这个断言颇有帮助,可是确定还想要检验表的实际内容,好核实是否全部值都写到了正确的列中。能够经过表断言来作到这一点。
为此,先定义一个 QueryTable 实例,从表名称和 SQL 查询派生出其内容,随后将其与一个基于文件/数组的数据集进行比较:
<?php class GuestbookTest extends PHPUnit_Extensions_Database_TestCase { public function testAddEntry() { $guestbook = new Guestbook(); $guestbook->addEntry("suzy", "Hello world!"); $queryTable = $this->getConnection()->createQueryTable( 'guestbook', 'SELECT * FROM guestbook' ); $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml") ->getTable("guestbook"); $this->assertTablesEqual($expectedTable, $queryTable); } } ?>
如今须要为这个断言编写Flat XML 文件 expectedBook.xml:
<?xml version="1.0" ?> <dataset> <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" /> <guestbook id="2" content="I like it!" user="nancy" created="2010-04-26 12:14:20" /> <guestbook id="3" content="Hello world!" user="suzy" created="2010-05-01 21:47:08" /> </dataset>
在整个时间长河中,只有特定的一秒钟内这个断言能够经过评定,在 2010–05–01 21:47:08。在数据库测试中,日期构成了一个特殊的问题。能够从这个断言中省略 “created” 列来规避失败。
为了让断言能得以经过, Flat XML 文件 expectedBook.xml 须要调整成大体相似这样:
<?xml version="1.0" ?> <dataset> <guestbook id="1" content="Hello buddy!" user="joe" /> <guestbook id="2" content="I like it!" user="nancy" /> <guestbook id="3" content="Hello world!" user="suzy" /> </dataset>
还得修正一下 QueryTable 的调用:
<?php $queryTable = $this->getConnection()->createQueryTable( 'guestbook', 'SELECT id, content, user FROM guestbook' ); ?>
利用 QueryTable,也能够对复杂查询的结果做出断言,只须要指定查询以及结果名称,并随后将其与某个数据集进行比较:
<?php class ComplexQueryTest extends PHPUnit_Extensions_Database_TestCase { public function testComplexQuery() { $queryTable = $this->getConnection()->createQueryTable( 'myComplexQuery', 'SELECT complexQuery...' ); $expectedTable = $this->createFlatXmlDataSet("complexQueryAssertion.xml") ->getTable("myComplexQuery"); $this->assertTablesEqual($expectedTable, $queryTable); } } ?>
固然能够一次性对多个表的状态做出断言,并将查询数据集与基于文件的数据集进行比较。有两种不一样的方式来进行数据集断言。
能够从自数据库链接创建数据库数据集,并将其与基于文件的数据集进行比较。
<?php class DataSetAssertionsTest extends PHPUnit_Extensions_Database_TestCase { public function testCreateDataSetAssertion() { $dataSet = $this->getConnection()->createDataSet(array('guestbook')); $expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml'); $this->assertDataSetsEqual($expectedDataSet, $dataSet); } } ?>
也能够自行构造数据集:
<?php class DataSetAssertionsTest extends PHPUnit_Extensions_Database_TestCase { public function testManualDataSetAssertion() { $dataSet = new PHPUnit_Extensions_Database_DataSet_QueryDataSet(); $dataSet->addTable('guestbook', 'SELECT id, content, user FROM guestbook'); // additional tables $expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml'); $this->assertDataSetsEqual($expectedDataSet, $dataSet); } } ?>
不,PHPUnit 要求在测试套件开始时全部数据库对象必须所有可用。数据库、表、序列、触发器还有视图,必须所有在运行测试套件以前建立好。
Doctrine 2 或 eZ Components 拥有强力的工具,能够按预约义的数据结构建立数据库,可是这些都必须和 PHPUnit 扩展模块对接以后才能自动在整个测试套件运行以前从新建立数据库。
因为每一个测试都会完全清空数据库,所以无须为每一个测试从新建立数据库。持久可用的数据库一样可以完美工做。
若是没有对 TestCase 中 getConnection()
方法所建立 PDO 实例进行缓存,那么每一个数据库测试都会增长一个或多个数据库链接。MySQL的默认配置只容许100个并发链接,其余供应商的数据库也都有各自的最大链接限制。
子章节 “使用你本身的抽象数据库 TestCase 类”展现了如何经过在全部测试中使用单个PDO实例缓存来防止发生此错误。
Gerard Meszaros 在 [Meszaros2007] 中介绍了测试替身的概念:
PHPUnit 提供的 getMockBuilder($type)
方法能够在测试中用来自动生成对象,此对象能够充当任意指定原版类型(接口或类名)的测试替身。在任何预期或要求使用原版类的实例对象的上下文中均可以使用这个测试替身对象来代替。
在默认状况下,原版类的全部方法都会被替换为只会返回 null
的伪实现(其中不会调用原版方法)。使用诸如 will($this->returnValue())
之类的方法能够对这些伪实如今被调用时应当返回什么值作出配置。
请注意,final
、private
和 static
方法没法对其进行上桩(stub)或模仿(mock)。PHPUnit 的测试替身功能将会忽略它们,并维持它们的原始行为。
请关注一下这个事实:参数管理方式已经修改过了。在以前的实现中,将会克隆对象的全部参数。这样就没法检查传递给方法的是不是同一个对象。例 9.15 展现了新的实现方式在什么状况下会很是有用。例 9.16展现了如何切换回以前的行为方式。
将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。能够用桩件(stub)来“替换掉被测系统所依赖的实际组件,这样测试就有了对被测系统的间接输入的控制点。这使得测试能强制安排被测系统的执行路径,不然被测系统可能没法执行”。
例 9.2展现了如何对方法的调用上桩以及如何设定返回值。首先用 PHPUnit_Framework_TestCase
类提供的 getMockBuilder()
方法来创建一个桩件对象,它表面看起来像是 SomeClass
类(例 9.1)的实例。随后用 PHPUnit 提供的 流畅式接口来指定桩件的行为。本质上,这意味着不须要创建多个临时对象而后再把它们捆到一块儿。取而代之的是范例中所示的链式方法调用。这使得代码更加易读并更加“流畅”。
例 9.2: 对某个方法的调用上桩,返回固定值
<?php require_once 'SomeClass.php'; class StubTest extends PHPUnit_Framework_TestCase { public function testStub() { // 为 SomeClass 类建立桩件。 $stub = $this->getMockBuilder('SomeClass') ->getMock(); // 配置桩件。 $stub->method('doSomething') ->willReturn('foo'); // 如今调用 $stub->doSomething() 将返回 'foo'。 $this->assertEquals('foo', $stub->doSomething()); } } ?>
仅当原始类中不包含名字为“method”的方法时,以上范例才能正常运行。
若是原始类包含名为“method”的方法,就必须用 $stub->expects($this->any())->method('doSomething')->willReturn('foo');
。
“在幕后”,当使用了 getMock()
方法时, PHPUnit 自动生成了一个新的 PHP 类来实现想要的行为。
例 9.3这个例子展现了如何用仿件生成器的流畅式接口来配置测试替身的生成。
例 9.3: 使用可用于配置生成的测试替身类的仿件生成器 API
<?php require_once 'SomeClass.php'; class StubTest extends PHPUnit_Framework_TestCase { public function testStub() { // 为 SomeClass 类建立桩件。 $stub = $this->getMockBuilder('SomeClass') ->disableOriginalConstructor() ->getMock(); // 配置桩件。 $stub->method('doSomething') ->willReturn('foo'); // 如今调用 $stub->doSomething() 将返回 'foo'。 $this->assertEquals('foo', $stub->doSomething()); } } ?>
如下是仿件生成器提供的方法列表:
setMethods(array $methods)
能够在仿件生成器对象上调用,来指定哪些方法将被替换为可配置的测试替身。其余方法的行为不会有所改变。若是调用 setMethods(null)
,那么没有方法会被替换。
setConstructorArgs(array $args)
可用于向原版类的构造函数(默认状况下不会被替换为伪实现)提供参数数组。
setMockClassName($name)
可用于指定生成的测试替身类的类名。
disableOriginalConstructor()
参数可用于禁用对原版类的构造方法的调用。
disableOriginalClone()
可用于禁用对原版类的克隆方法的调用。
disableAutoload()
可用于在测试替身类的生成期间禁用 __autoload()
。
在以前的例子中,用 willReturn($value)
返回简单值。这个简短的语法至关于 will($this->returnValue($value))
。而在这个长点的语法中,能够使用变量,从而实现更复杂的上桩行为。
有时想要将(未改变的)方法调用时所使用的参数之一做为桩件的方法的调用结果来返回。 例 9.4展现了如何用 returnArgument()
代替 returnValue()
来作到这点。
例 9.4: 对某个方法的调用上桩,返回参数之一
<?php require_once 'SomeClass.php'; class StubTest extends PHPUnit_Framework_TestCase { public function testReturnArgumentStub() { // 为 SomeClass 类建立桩件。 $stub = $this->getMockBuilder('SomeClass') ->getMock(); // 配置桩件。 $stub->method('doSomething') ->will($this->returnArgument(0)); // stub->doSomething('foo') 返回 'foo' $this->assertEquals('foo', $stub->doSomething('foo')); // $stub->doSomething('bar') 返回 'bar' $this->assertEquals('bar', $stub->doSomething('bar')); } } ?>
在用流畅式接口进行测试时,让某个已上桩的方法返回对桩件对象的引用有时会颇有用。例 9.5展现了如何用 returnSelf()
来作到这点。
例 9.5: 对方法的调用上桩,返回对桩件对象的引用
<?php require_once 'SomeClass.php'; class StubTest extends PHPUnit_Framework_TestCase { public function testReturnSelf() { // 为 SomeClass 类建立桩件。 $stub = $this->getMockBuilder('SomeClass') ->getMock(); // 配置桩件。 $stub->method('doSomething') ->will($this->returnSelf()); // $stub->doSomething() 返回 $stub $this->assertSame($stub, $stub->doSomething()); } } ?>
有时候,上桩的方法须要根据预约义的参数清单来返回不一样的值。能够用 returnValueMap()
方法将参数和相应的返回值关联起来创建映射。范例参见例 9.6。
例 9.6: 对方法的调用上桩,按照映射肯定返回值
<?php require_once 'SomeClass.php'; class StubTest extends PHPUnit_Framework_TestCase { public function testReturnValueMapStub() { // 为 SomeClass 类建立桩件。 $stub = $this->getMockBuilder('SomeClass') ->getMock(); // 建立从参数到返回值的映射。 $map = array( array('a', 'b', 'c', 'd'), array('e', 'f', 'g', 'h') ); // 配置桩件。 $stub->method('doSomething') ->will($this->returnValueMap($map)); // $stub->doSomething() 根据提供的参数返回不一样的值。 $this->assertEquals('d', $stub->doSomething('a', 'b', 'c')); $this->assertEquals('h', $stub->doSomething('e', 'f', 'g')); } } ?>
若是上桩的方法须要返回计算获得的值而不是固定值(参见 returnValue()
)或某个(未改变的)参数(参见 returnArgument()
),能够用 returnCallback()
来让上桩的方法返回回调函数或方法的结果。范例参见例 9.7。
例 9.7: 对方法的调用上桩,由回调生成返回值
<?php require_once 'SomeClass.php'; class StubTest extends PHPUnit_Framework_TestCase { public function testReturnCallbackStub() { // 为 SomeClass 类建立桩件。 $stub = $this->getMockBuilder('SomeClass') ->getMock(); // 配置桩件。 $stub->method('doSomething') ->will($this->returnCallback('str_rot13')); // $stub->doSomething($argument) 返回 str_rot13($argument) $this->assertEquals('fbzrguvat', $stub->doSomething('something')); } } ?>
相比于创建回调方法,有一个更简单的选择是直接给出指望返回值的列表。能够用 onConsecutiveCalls()
方法来作到这个。范例参见 例 9.8。
例 9.8: 对方法的调用上桩,按照指定顺序返回列表中的值
<?php require_once 'SomeClass.php'; class StubTest extends PHPUnit_Framework_TestCase { public function testOnConsecutiveCallsStub() { // 为 SomeClass 类建立桩件。 $stub = $this->getMockBuilder('SomeClass') ->getMock(); // 配置桩件。 $stub->method('doSomething') ->will($this->onConsecutiveCalls(2, 3, 5, 7)); // $stub->doSomething() 每次返回值都不一样 $this->assertEquals(2, $stub->doSomething()); $this->assertEquals(3, $stub->doSomething()); $this->assertEquals(5, $stub->doSomething()); } } ?>
除了返回一个值以外,上桩的方法还能抛出一个异常。例 9.9展现了如何用 throwException()
作到这点。
例 9.9: 对方法的调用上桩,抛出异常
<?php require_once 'SomeClass.php'; class StubTest extends PHPUnit_Framework_TestCase { public function testThrowExceptionStub() { // 为 SomeClass 类建立桩件。 $stub = $this->getMockBuilder('SomeClass') ->getMock(); // 配置桩件。 $stub->method('doSomething') ->will($this->throwException(new Exception)); // $stub->doSomething() 抛出异常 $stub->doSomething(); } } ?>
另外,也能够自行编写桩件,并在此过程当中改善设计。在系统中被普遍使用的资源是经过单个外观(facade)来访问的,所以很容易就能用桩件替换掉资源。例如,将散落在代码各处的对数据库的直接调用替换为单个 Database
对象,这个对象实现了 IDatabase
接口。接下来,就能够建立实现了 IDatabase
的桩件并在测试中使用之。甚至能够建立一个选项来控制是用桩件仍是用真实数据库来运行测试,这样测试就既能在开发过程当中用做本地测试,又能在实际数据库环境中进行集成测试。
须要上桩的功能每每集中在同一个对象中,这就改善了内聚度。将功能经过单一且一致的界面呈现出来,就下降了这部分与系统其余部分之间的耦合度。
将对象替换为能验证预期行为(例如断言某个方法必会被调用)的测试替身的实践方法称为模仿(mocking)。
能够用 仿件对象(mock object)“做为观察点来核实被测试系统在测试中的间接输出。一般,仿件对象还须要包括桩件的功能,由于若是测试还没有失败则仿件对象须要向被测系统返回一些值,可是其重点仍是在对间接输出的核实上。所以,仿件对象远不止是桩件加断言,它是以一种从根本上彻底不一样的方式来使用的”(Gerard Meszaros)。
PHPUnit只会对在某个测试的做用域内生成的仿件对象进行自动校验。诸如在数据供给器内生成或用@depends
标注注入测试的仿件对象,PHPUnit并不会自动对其进行校验。
这有个例子:假设须要测试的当前方法,在例子中是 update()
,确实在一个观察着另一个对象的对象中上被调用了。例 9.10展现了被测系统(SUT)中 Subject
和 Observer
两个类的代码。
例 9.10: 被测系统(SUT)中 Subject 与 Observer 类的代码
<?php class Subject { protected $observers = array(); protected $name; public function __construct($name) { $this->name = $name; } public function getName() { return $this->name; } public function attach(Observer $observer) { $this->observers[] = $observer; } public function doSomething() { // 作点什么。 // ... // 通知观察者。 $this->notify('something'); } public function doSomethingBad() { foreach ($this->observers as $observer) { $observer->reportError(42, 'Something bad happened', $this); } } protected function notify($argument) { foreach ($this->observers as $observer) { $observer->update($argument); } } // 其余方法。 } class Observer { public function update($argument) { // 作点什么。 } public function reportError($errorCode, $errorMessage, Subject $subject) { // 作点什么。 } // 其余方法。 } ?>
例 9.11展现了如何用仿件对象来测试 Subject
和 Observer
对象之间的互动。
首先用 PHPUnit_Framework_TestCase
类提供的 getMock()
方法创建 Observer
的仿件对象。因为给出了一个数组作为 getMock()
方法的第二(可选)参数,Observer
类只有 update()
方法会被替换为仿实现。
因为关注的是检验某个方法是否被调用,以及调用时具体所使用的参数,所以引入 expects()
与 with()
方法来指明此交互应该是什么样的。
例 9.11: 测试某个方法会以特定参数被调用一次
<?php class SubjectTest extends PHPUnit_Framework_TestCase { public function testObserversAreUpdated() { // 为 Observer 类创建仿件对象,只模仿 update() 方法。 $observer = $this->getMockBuilder('Observer') ->setMethods(array('update')) ->getMock(); // 创建预期情况:update() 方法将会被调用一次, // 而且将以字符串 'something' 为参数。 $observer->expects($this->once()) ->method('update') ->with($this->equalTo('something')); // 建立 Subject 对象,并将模仿的 Observer 对象链接其上。 $subject = new Subject('My subject'); $subject->attach($observer); // 在 $subject 对象上调用 doSomething() 方法, // 预期将以字符串 'something' 为参数调用 // Observer 仿件对象的 update() 方法。 $subject->doSomething(); } } ?>
with()
方法能够携带任何数量的参数,对应于被模仿的方法的参数数量。能够对方法的参数指定更加高等的约束而不只是简单的匹配。
例 9.12: 测试某个方法将会以特定数量的参数进行调用,而且对各个参数以多种方式进行约束
<?php class SubjectTest extends PHPUnit_Framework_TestCase { public function testErrorReported() { // 为 Observer 类创建仿件,对 reportError() 方法进行模仿 $observer = $this->getMockBuilder('Observer') ->setMethods(array('reportError')) ->getMock(); $observer->expects($this->once()) ->method('reportError') ->with( $this->greaterThan(0), $this->stringContains('Something'), $this->anything() ); $subject = new Subject('My subject'); $subject->attach($observer); // doSomethingBad() 方法应当会经过(observer的)reportError()方法 //向 observer 报告错误。 $subject->doSomethingBad(); } } ?>
withConsecutive()
方法能够接受任意多个数组做为参数,具体数量取决于欲测试的调用。每一个数组都都是对被仿方法的相应参数的一组约束,就像 with()
中那样。
例 9.13: 测试某个方法将会以特定参数被调用二次
<?php class FooTest extends PHPUnit_Framework_TestCase { public function testFunctionCalledTwoTimesWithSpecificArguments() { $mock = $this->getMockBuilder('stdClass') ->setMethods(array('set')) ->getMock(); $mock->expects($this->exactly(2)) ->method('set') ->withConsecutive( array($this->equalTo('foo'), $this->greaterThan(0)), array($this->equalTo('bar'), $this->greaterThan(0)) ); $mock->set('foo', 21); $mock->set('bar', 48); } } ?>
callback()
约束用来进行更加复杂的参数校验。此约束的惟一参数是一个 PHP 回调项(callback)。此 PHP 回调项接受须要校验的参数做为其惟一参数,并应当在参数经过校验时返回 TRUE
,不然返回 FALSE
。
例 9.14: 更加复杂的参数校验
<?php class SubjectTest extends PHPUnit_Framework_TestCase { public function testErrorReported() { // 为 Observer 类创建仿件,模仿 reportError() 方法 $observer = $this->getMockBuilder('Observer') ->setMethods(array('reportError')) ->getMock(); $observer->expects($this->once()) ->method('reportError') ->with($this->greaterThan(0), $this->stringContains('Something'), $this->callback(function($subject){ return is_callable(array($subject, 'getName')) && $subject->getName() == 'My subject'; })); $subject = new Subject('My subject'); $subject->attach($observer); // doSomethingBad() 方法应当会经过(observer的)reportError()方法 //向 observer 报告错误。 $subject->doSomethingBad(); } } ?>
例 9.15: 测试某个方法将会被调用一次,而且以某个特定对象做为参数。
<?php class FooTest extends PHPUnit_Framework_TestCase { public function testIdenticalObjectPassed() { $expectedObject = new stdClass; $mock = $this->getMockBuilder('stdClass') ->setMethods(array('foo')) ->getMock(); $mock->expects($this->once()) ->method('foo') ->with($this->identicalTo($expectedObject)); $mock->foo($expectedObject); } } ?>
例 9.16: 建立仿件对象时启用参数克隆
<?php class FooTest extends PHPUnit_Framework_TestCase { public function testIdenticalObjectPassed() { $cloneArguments = true; $mock = $this->getMockBuilder('stdClass') ->enableArgumentCloning() ->getMock(); // 如今仿件将对参数进行克隆,所以 identicalTo 约束将会失败。 } } ?>
表 A.1列出了能够应用于方法参数的各类约束,表 9.1列出了能够用于指定调用次数的各类匹配器。
表 9.1. 匹配器
匹配器 | 含义 |
---|---|
PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any() |
返回一个匹配器,当被评定的方法执行0次或更屡次(即任意次数)时匹配成功。 |
PHPUnit_Framework_MockObject_Matcher_InvokedCount never() |
返回一个匹配器,当被评定的方法从未执行时匹配成功。 |
PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce() |
返回一个匹配器,当被评定的方法执行至少一次时匹配成功。 |
PHPUnit_Framework_MockObject_Matcher_InvokedCount once() |
返回一个匹配器,当被评定的方法执行刚好一次时匹配成功。 |
PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count) |
返回一个匹配器,当被评定的方法执行刚好 $count 次时匹配成功。 |
PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index) |
返回一个匹配器,当被评定的方法是第 $index 个执行的方法时匹配成功。 |
at()
匹配器的 $index
参数指的是对给定仿件对象的全部方法的调用的索引,从零开始。使用这个匹配器要谨慎,由于它可能致使测试因为与具体的实现细节过度紧密绑定而变得脆弱。
Prophecy 是个“极为自我却又很是强大且灵活的 PHP 对象模仿框架。虽然一开始是为了知足 phpspec2 的须要而创建的,但它足够灵活,能够用最小代价用于任何测试框架内。”
从版本 4.5 开始,PHPUnit 为用 Prophecy 创建测试替身提供了内建支持。例 9.17展现了例 9.11中展现的测试应该如何用 Prophecy 的的预言式理念方式来达到一样的效果:
例 9.17: 测试某个方法会以特定参数被调用一次
<?php class SubjectTest extends PHPUnit_Framework_TestCase { public function testObserversAreUpdated() { $subject = new Subject('My subject'); // 为 Observer 类创建预言(prophecy)。 $observer = $this->prophesize('Observer'); // 创建预期情况:update() 方法将会被调用一次, // 而且将以字符串 'something' 为参数。 $observer->update('something')->shouldBeCalled(); // 揭示预言,并将仿件对象连接到主体上。 $subject->attach($observer->reveal()); // 在 $subject 对象上调用 doSomething() 方法, // 预期将以字符串 'something' 为参数调用 // Observer 仿件对象的 update() 方法。 $subject->doSomething(); } } ?>
更多关于如何用这个测试替身框架来建立、配置及使用桩件、谍件、仿件的细节,请参考 Prophecy 的 文档。
getMockForTrait()
方法返回一个使用了特定特质(trait)的仿件对象。给定特质的全部抽象方法将都被模仿。这样就能对特质的具体方法进行测试。
例 9.18: 对特质的具体方法进行测试
<?php trait AbstractTrait { public function concreteMethod() { return $this->abstractMethod(); } public abstract function abstractMethod(); } class TraitClassTest extends PHPUnit_Framework_TestCase { public function testConcreteMethod() { $mock = $this->getMockForTrait('AbstractTrait'); $mock->expects($this->any()) ->method('abstractMethod') ->will($this->returnValue(TRUE)); $this->assertTrue($mock->concreteMethod()); } } ?>
getMockForAbstractClass()
方法返回一个抽象类的仿件对象。给定抽象类的全部抽象方法将都被模仿。这样就能对抽象类的具体方法进行测试。
例 9.19: 对抽象类的具体方法进行测试
<?php abstract class AbstractClass { public function concreteMethod() { return $this->abstractMethod(); } public abstract function abstractMethod(); } class AbstractClassTest extends PHPUnit_Framework_TestCase { public function testConcreteMethod() { $stub = $this->getMockForAbstractClass('AbstractClass'); $stub->expects($this->any()) ->method('abstractMethod') ->will($this->returnValue(TRUE)); $this->assertTrue($stub->concreteMethod()); } } ?>
当应用程序须要和 web 服务进行交互时,会想要在不与 web 服务进行实际交互的状况下对其进行测试。为了简单地对 web 服务进行上桩或模仿,能够像使用 getMock()
(见上文)那样使用 getMockFromWsdl()
。惟一的区别是 getMockFromWsdl()
所返回的桩件或者仿件是基于以 WSDL 描述的 web 服务,而 getMock()
返回的桩件或者仿件是基于 PHP 类或接口的。
例 9.20展现了如何用 getMockFromWsdl()
来对(例如)GoogleSearch.wsdl
中描述的 web 服务上桩。
例 9.20: 对 web 服务上桩
<?php class GoogleTest extends PHPUnit_Framework_TestCase { public function testSearch() { $googleSearch = $this->getMockFromWsdl( 'GoogleSearch.wsdl', 'GoogleSearch' ); $directoryCategory = new stdClass; $directoryCategory->fullViewableName = ''; $directoryCategory->specialEncoding = ''; $element = new stdClass; $element->summary = ''; $element->URL = 'https://phpunit.de/'; $element->snippet = '...'; $element->title = '<b>PHPUnit</b>'; $element->cachedSize = '11k'; $element->relatedInformationPresent = TRUE; $element->hostName = 'phpunit.de'; $element->directoryCategory = $directoryCategory; $element->directoryTitle = ''; $result = new stdClass; $result->documentFiltering = FALSE; $result->searchComments = ''; $result->estimatedTotalResultsCount = 3.9000; $result->estimateIsExact = FALSE; $result->resultElements = array($element); $result->searchQuery = 'PHPUnit'; $result->startIndex = 1; $result->endIndex = 1; $result->searchTips = ''; $result->directoryCategories = array(); $result->searchTime = 0.248822; $googleSearch->expects($this->any()) ->method('doGoogleSearch') ->will($this->returnValue($result)); /** * $googleSearch->doGoogleSearch() 将会返回上桩的结果, * web 服务的 doGoogleSearch() 方法不会被调用。 */ $this->assertEquals( $result, $googleSearch->doGoogleSearch( '00000000000000000000000000000000', 'PHPUnit', 0, 1, FALSE, '', FALSE, '', '', '' ) ); } } ?>
vfsStream 是对虚拟文件系统 的 流包覆器(stream wrapper),能够用于模仿真实文件系统,在单元测试中可能会有所助益。
若是使用 Composer 来管理项目的依赖关系,那么只需简单的在项目的 composer.json
文件中加一条对 mikey179/vfsStream
的依赖关系便可。如下是一个最小化的 composer.json
文件例子,只定义了一条对 PHPUnit 4.6 与 vfsStream 的开发时(development-time)依赖:
{ "require-dev": { "phpunit/phpunit": "~4.6", "mikey179/vfsStream": "~1" } }
例 9.21展现了一个与文件系统交互的类。
例 9.21: 一个与文件系统交互的类
<?php class Example { protected $id; protected $directory; public function __construct($id) { $this->id = $id; } public function setDirectory($directory) { $this->directory = $directory . DIRECTORY_SEPARATOR . $this->id; if (!file_exists($this->directory)) { mkdir($this->directory, 0700, TRUE); } } }?>
若是不使用诸如 vfsStream 这样的虚拟文件系统,就没法在隔离外部影响的状况下对 setDirectory()
方法进行测试(参见 例 9.22)。
例 9.22: 对一个与文件系统交互的类进行测试
<?php require_once 'Example.php'; class ExampleTest extends PHPUnit_Framework_TestCase { protected function setUp() { if (file_exists(dirname(__FILE__) . '/id')) { rmdir(dirname(__FILE__) . '/id'); } } public function testDirectoryIsCreated() { $example = new Example('id'); $this->assertFalse(file_exists(dirname(__FILE__) . '/id')); $example->setDirectory(dirname(__FILE__)); $this->assertTrue(file_exists(dirname(__FILE__) . '/id')); } protected function tearDown() { if (file_exists(dirname(__FILE__) . '/id')) { rmdir(dirname(__FILE__) . '/id'); } } } ?>
上面的方法有几个缺点:
和任何其余外部资源同样,文件系统可能会间歇性的出现一些问题,这使得和它交互的测试变得不可靠。
在 setUp()
和 tearDown()
方法中,必须确保这个目录在测试前和测试后均不存在。
若是测试在 tearDown()
方法被调用以前就终止了,这个目录就会遗留在文件系统中。
例 9.23展现了如何在对与文件系统交互的类进行的测试中使用 vfsStream 来模仿文件系统。
例 9.23: 在对与文件系统交互的类进行的测试中模仿文件系统
<?php require_once 'vfsStream/vfsStream.php'; require_once 'Example.php'; class ExampleTest extends PHPUnit_Framework_TestCase { public function setUp() { vfsStreamWrapper::register(); vfsStreamWrapper::setRoot(new vfsStreamDirectory('exampleDir')); } public function testDirectoryIsCreated() { $example = new Example('id'); $this->assertFalse(vfsStreamWrapper::getRoot()->hasChild('id')); $example->setDirectory(vfsStream::url('exampleDir')); $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('id')); } } ?>
这有几个优势:
测试自己更加简洁。
vfsStream 让开发者可以彻底控制被测代码所处的文件系统环境。
因为文件系统操做再也不对真实文件系统进行操做,tearDown()
方法中的清理操做再也不须要了。
你总能编写更多测试。可是很快就会发现,在全部想得出来的测试中只有很小一部分是真正有用的。须要编写的是那些以为能运做但却失败或以为必将失败但却成功的测试。另一种思考方式是从成本/收益的关系上去考量。须要编写的是可以给出反馈信息的测试。 |
||
--Erich Gamma |
当须要对软件的内部结构进行更改时,你其实是要在不影响其可见行为的状况下让它更加容易理解、更加易于修改,测试套件对于安全地进行这些所谓的重构而言是很是宝贵的。不然,你可能在重组过程当中将系统搞坏而不自知。
在使用单元测试来确认重构的转换步骤中确实保持原有行为而且没有引入错误时,如下状况有助于改进项目的编码与设计:
全部单元测试均正确运行。
代码传达其设计原则。
代码没有冗余。
代码所包含的类和方法的数量降至最低。
当须要向系统内添加新的功能时,首先为其编写测试。而后,当测试可以正常运行就标志着开发完成了。下一章将详细讨论这种作法。
当看到缺陷报告时,你可能会有尽快修复错误的冲动。经验代表,这种冲动不是好事,由于修复一个缺陷时极可能致使另一个缺陷。
下列操做能够帮你压住冲动:
确认可以重现此缺陷。
在代码中寻找此缺陷的最小规模表达。例如,若是在输出中有一个数字看起来不对,那么就寻找算出此数字的那个对象。
编写一个目前会失败而缺陷修复后将会成功的自动测试。
修复缺陷。
寻找缺陷的最小可靠重现使你有机会去真正检查缺陷的缘由。当修复了缺陷以后,所编写的测试则有助于提升缺陷真正被修复的概率,由于新加入的测试下降了将来修改代码时又破坏此修复的可能性。而以前所编写的全部测试则下降了在不经意间致使其余问题的可能性。
进行单元测试带来了不少好处:
总之,进行集成单元测试下降了任何修改的成本与风险。这使得项目可以更快而且更有信心地进行[...]重大架构改良[...]。 |
||
--Benjamin Smedberg |
计算机科学中所说的代码覆盖率是一种用于衡量特定测试套件对程序源代码测试程度的指标。拥有高代码覆盖率的程序相较于低代码低几率的程序而言测试的更加完全、包含软件 bug 的可能性更低。 |
||
--Wikipedia |
在本章中,你将学到 PHPUnit 中与代码覆盖率相关的一切功能。经过这部分功能,可以了解在测试运行过程当中执行了生产代码的哪些部分。它使用了 PHP_CodeCoverage 组件,而这个组件又使用了 PHP 的 Xdebug 扩展所提供的代码覆盖率功能。
Xdebug 不随 PHPUnit 分发。若是在运行测试时收到了 Xdebug 扩展未加载的提示,就意味着 Xdebug 未安装或者未正确配置。在使用 PHPUnit 的代码覆盖率分析功能以前,须要阅读下 Xdebug 安装指南。
PHPUnit 能够生成基于 HTML 的代码覆盖率报告,同时也能生成好几种(Clover、Crap4J、PHPUnit)基于XML的代码覆盖率信息记录文件。代码覆盖率信息也能以文本格式提供(同时能够输出到STDOUT)或以PHP代码格式输出以供进一步处理。
第 3 章中列出了各类控制代码覆盖率功能的命令行参数供参考,同时“Logging (日志记录)”一节中能够找到其余相关的配置信息。
目前存在多种软件衡量标准用于衡量代码覆盖率:
行覆盖率(Line Coverage)按单个可执行行是否已执行到进行计量。
函数与方法覆盖率(Function and Method Coverage)按单个函数或方法是否已调用到进行计量。仅当函数或方法的全部可执行行所有已覆盖时 PHP_CodeCoverage 才将其视为已覆盖。
类与特质覆盖率(Class and Trait Coverage)按单个类或特质的全部方法是否所有已覆盖进行计量。仅当一个类或性状的全部方法所有已覆盖时 PHP_CodeCoverage 才将其视为已覆盖。
Opcode 覆盖率按函数或方法对应的每条 opcode 在运行测试套件时是否执行到进行计量。一行(PHP的)代码一般会编译获得多条 opcode。进行行覆盖率计量时,只要其中任何一条 opcode 被执行就视为此行已覆盖。
分支覆盖率(Branch Coverage)按控制结构的分支进行计量。测试套件运行时每一个控制结构的布尔表达式求值为 true
和 false
各自计为一个分支。
路径覆盖率(Path Coverage)按测试套件运行时函数或者方法内部所经历的执行路径进行计量。一个执行路径指的是从进入函数或方法一直到离开的过程当中通过各个分支的特定序列。
变动风险反模式(CRAP)指数(Change Risk Anti-Patterns (CRAP) Index)是基于代码单元的圈复杂度(cyclomatic complexity)与代码覆盖率计算得出的。不太复杂并具备恰当测试覆盖率的代码将得出较低的CRAP指数。能够经过编写测试或重构代码来下降其复杂性的方式来下降CRAP指数。
目前 PHP_CodeCoverage 尚不支持 Opcode覆盖率、分支覆盖率 及 路径覆盖率。
为了告诉 PHPUnit 哪些源代码文件要包含在代码覆盖率报告中,必须配置白名单。能够用命令行选项 --whitelist
或经过配置文件(参见 “Whitelisting Files for Code Coverage”一节)来完成。
能够在 PHPUnit 的配置信息中设置 addUncoveredFilesFromWhitelist="true"
来将白名单中包含的全部文件所有加入到代码覆盖率报告中(参见“Whitelisting Files for Code Coverage”一节)。这样能够把彻底没有测试到的文件也一并包含到报告中。若是须要知道这些未被覆盖文件中有哪些行是可执行的,须要同时在 PHPUnit 的配置信息中设置 processUncoveredFilesFromWhitelist="true"
(参见“Whitelisting Files for Code Coverage”一节)。
请注意,当设置了 processUncoveredFilesFromWhitelist="true"
时将对源代码文件进行载入,这在某些状况下可能致使问题,好比,源代码文件包含有处于类或者函数做用域以外的代码。
有时,一些代码块是没法对其进行测试的,所以但愿在代码覆盖率分析中忽略它们。在 PHPUnit 中能够用 @codeCoverageIgnore
、@codeCoverageIgnoreStart
与 @codeCoverageIgnoreEnd
标注来作到这点,如例 11.1中所示。
例 11.1: 使用 @codeCoverageIgnore
、@codeCoverageIgnoreStart
与 @codeCoverageIgnoreEnd
标注
<?php /** * @codeCoverageIgnore */ class Foo { public function bar() { } } class Bar { /** * @codeCoverageIgnore */ public function foo() { } } if (FALSE) { // @codeCoverageIgnoreStart print '*'; // @codeCoverageIgnoreEnd } exit; // @codeCoverageIgnore ?>
代码中被忽略掉的行(用标注标记为忽略)将会计为已执行(若是它们是可执行的),而且不会在代码覆盖状况中被高亮标记。
@covers
标注(参见 表 B.1)能够用在测试代码中来指明测试方法想要对哪些方法进行测试。若是提供了这个信息,则只有指定方法的代码覆盖率信息会被统计。 例 11.2展现了一个例子。
例 11.2: 在测试中指明欲覆盖哪些方法
<?php class BankAccountTest extends PHPUnit_Framework_TestCase { protected $ba; protected function setUp() { $this->ba = new BankAccount; } /** * @covers BankAccount::getBalance */ public function testBalanceIsInitiallyZero() { $this->assertEquals(0, $this->ba->getBalance()); } /** * @covers BankAccount::withdrawMoney */ public function testBalanceCannotBecomeNegative() { try { $this->ba->withdrawMoney(1); } catch (BankAccountException $e) { $this->assertEquals(0, $this->ba->getBalance()); return; } $this->fail(); } /** * @covers BankAccount::depositMoney */ public function testBalanceCannotBecomeNegative2() { try { $this->ba->depositMoney(-1); } catch (BankAccountException $e) { $this->assertEquals(0, $this->ba->getBalance()); return; } $this->fail(); } /** * @covers BankAccount::getBalance * @covers BankAccount::depositMoney * @covers BankAccount::withdrawMoney */ public function testDepositWithdrawMoney() { $this->assertEquals(0, $this->ba->getBalance()); $this->ba->depositMoney(1); $this->assertEquals(1, $this->ba->getBalance()); $this->ba->withdrawMoney(1); $this->assertEquals(0, $this->ba->getBalance()); } } ?>
同时,能够用 @coversNothing
标注来指明一个测试不覆盖任何方法(参见“@coversNothing”一节)。这能够在编写集成测试时用于确保代码覆盖所有来自单元测试。
例 11.3: 指明测试不欲覆盖任何方法
<?php class GuestbookIntegrationTest extends PHPUnit_Extensions_Database_TestCase { /** * @coversNothing */ public function testAddEntry() { $guestbook = new Guestbook(); $guestbook->addEntry("suzy", "Hello world!"); $queryTable = $this->getConnection()->createQueryTable( 'guestbook', 'SELECT * FROM guestbook' ); $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml") ->getTable("guestbook"); $this->assertTablesEqual($expectedTable, $queryTable); } } ?>
本节中展现了一些值得注意的边缘状况,在这些边缘状况中可能出现使人迷惑的代码覆盖率信息。
例 11.4:
<?php // 由于覆盖率是“基于行”而不是基于语句的, // 每行只会有一种覆盖状态 if (false) this_function_call_shows_up_as_covered(); // 因为代码覆盖率的内部工做方式,这两行显得很特殊。 // 这一行会显示为非可执行 if (false) // 这一行会显示为已覆盖, // 其实是上一行的 if 语句的覆盖信息显示在这了! will_also_show_up_as_covered(); // 要避免这种状况,必须使用大括号 if (false) { this_call_will_never_show_up_as_covered(); } ?>
一旦习惯了编写自动测试,就可能会发现测试的更多用途。这有一些例子。
一般,在使用了诸如极限编程之类的敏捷流程的项目中,文档每每没法跟上项目设计与代码的频繁变动。极限编程要求群体代码全部权(collective code ownership),所以全部开发者都须要知道整个系统是如何工做的。若是你足够训练有素,为测试使用了“能说明问题的名称(speaking names)”来描述各个类应当干什么,那么就能够用 PHPUnit 的 TestDox 功能来基于项目的测试生成项目的自动文档。这个文档可以就项目中的各个类应当起什么做用给开发者一份概述。
PHPUnit 的 TestDox 功能着眼于测试类及其全部测试方法的名称,将它们驼峰式大小写(camel case)拼写的 PHP 名称转换为句子:testBalanceIsInitiallyZero()
转化为 "Balance is initially zero(初始结余为零)"。若是有多个测试方法的名字互相之间的差别只是一个或多个数字的后缀,例如 testBalanceCannotBecomeNegative()
和 testBalanceCannotBecomeNegative2()
,假如全部这些测试都成功,句子"Balance cannot become negative(结余不能变为负数)"只会出现一次。
来看一下从 BankAccount
类生成的敏捷文档:
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. BankAccount [x] Balance is initially zero [x] Balance cannot become negativephpunit --testdox BankAccountTest
另外,敏捷文档也能够以 HTML 或纯文本格式生成,并写入文件中,用 --testdox-html
和 --testdox-text
参数便可。
敏捷文档能够用于将对项目所使用的外部包所作出的假设文档化。使用外部包,你就暴露于这个包的行为与你所预期的不一样的风险中,而且包的将来版本可能在你所不知道的状况下有微妙的改变并破坏你的代码。每次作出假设时就编写一个对应的测试能够处理这些风险。若是测试成功,那么假设就有效。若是全部的假设都经过测试来文档化,外部包在将来发布新版本就不会引发忧虑:若是测试成功,那么系统就应当能继续正常运做。
一旦用测试将假设文档化,你就拥有了测试。包的提供者——你作假设的对象——对你的测试一无所知。若是打算与包的提供者有更亲密的关系,能够用测试来沟通与协调你的活动。
当你愿意和包的提供着协调你的活动时,大家能够共同编写测试。经过这样的方式,测试可以展示出尽量多的假设。隐藏的假设是在给合做判死刑。利用测试,你精确的将对所提供的包的预期文档化。提供者在全部测试顺利运行时就知道包已经完整了。
经过使用桩件(参见本书前面关于“仿件对象”的那一章),你能够更好的与供应商解耦:供应商的工做就是让测试可以运行于包的实际实现上;你的工做则是让测试可以运行于你本身的代码上。在你拿到包的实际实现前,使用桩件对象。经过这种方式,两个团队能够互相独立的进行开发。
PHPUnit 所生成的测试结果 XML 日志文件是基于 JUnit task for Apache Ant 所使用的 XML 日志的。下面的例子展现了 ArrayTest
中的测试所生成的 XML 日志文件:
<?xml version="1.0" encoding="UTF-8"?> <testsuites> <testsuite name="ArrayTest" file="/home/sb/ArrayTest.php" tests="2" assertions="2" failures="0" errors="0" time="0.016030"> <testcase name="testNewArrayIsEmpty" class="ArrayTest" file="/home/sb/ArrayTest.php" line="6" assertions="1" time="0.008044"/> <testcase name="testArrayContainsAnElement" class="ArrayTest" file="/home/sb/ArrayTest.php" line="15" assertions="1" time="0.007986"/> </testsuite> </testsuites>
如下 XML 日志文件是由名为 FailureErrorTest
的测试用例类中的两个测试 testFailure
和 testError
所生成的,展现了失败和错误是如何表示的:
<?xml version="1.0" encoding="UTF-8"?> <testsuites> <testsuite name="FailureErrorTest" file="/home/sb/FailureErrorTest.php" tests="2" assertions="1" failures="1" errors="1" time="0.019744"> <testcase name="testFailure" class="FailureErrorTest" file="/home/sb/FailureErrorTest.php" line="6" assertions="1" time="0.011456"> <failure type="PHPUnit_Framework_ExpectationFailedException"> testFailure(FailureErrorTest) Failed asserting that <integer:2> matches expected value <integer:1>. /home/sb/FailureErrorTest.php:8 </failure> </testcase> <testcase name="testError" class="FailureErrorTest" file="/home/sb/FailureErrorTest.php" line="11" assertions="0" time="0.008288"> <error type="Exception">testError(FailureErrorTest) Exception: /home/sb/FailureErrorTest.php:13 </error> </testcase> </testsuite> </testsuites>
Test Anything Protocol (TAP) 是 Perl 与测试模块之间所使用的简单的基于文本的接口。下面的例子展现了 ArrayTest
中的测试所生成的 TAP 日志文件:
TAP version 13 ok 1 - testNewArrayIsEmpty(ArrayTest) ok 2 - testArrayContainsAnElement(ArrayTest) 1..2
如下 TAP 日志文件是由名为 FailureErrorTest
的测试用例类中的两个测试 testFailure
和 testError
所生成的,展现了失败和错误是如何表示的:
TAP version 13 not ok 1 - Failure: testFailure(FailureErrorTest) --- message: 'Failed asserting that <integer:2> matches expected value <integer:1>.' severity: fail data: got: 2 expected: 1 ... not ok 2 - Error: testError(FailureErrorTest) 1..2
JavaScript 对象表示法 (JSON)是轻量级的数据交换格式。下面的例子展现了 ArrayTest
中的测试所生成的 JSON 讯息:
{"event":"suiteStart","suite":"ArrayTest","tests":2} {"event":"test","suite":"ArrayTest", "test":"testNewArrayIsEmpty(ArrayTest)","status":"pass", "time":0.000460147858,"trace":[],"message":""} {"event":"test","suite":"ArrayTest", "test":"testArrayContainsAnElement(ArrayTest)","status":"pass", "time":0.000422954559,"trace":[],"message":""}
如下 JSON 讯息是由名为 FailureErrorTest
的测试用例类中的两个测试 testFailure
和 testError
所生成的,展现了失败和错误是如何表示的:
{"event":"suiteStart","suite":"FailureErrorTest","tests":2} {"event":"test","suite":"FailureErrorTest", "test":"testFailure(FailureErrorTest)","status":"fail", "time":0.0082459449768066,"trace":[], "message":"Failed asserting that <integer:2> is equal to <integer:1>."} {"event":"test","suite":"FailureErrorTest", "test":"testError(FailureErrorTest)","status":"error", "time":0.0083680152893066,"trace":[],"message":""}
PHPUnit 所生成的 XML 格式代码覆盖率信息日志记录不严格地基于 Clover. 所使用的 XML 日志的。下面的例子展现了 BankAccountTest
中的测试所生成的 XML 日志文件:
<?xml version="1.0" encoding="UTF-8"?> <coverage generated="1184835473" phpunit="3.6.0"> <project name="BankAccountTest" timestamp="1184835473"> <file name="/home/sb/BankAccount.php"> <class name="BankAccountException"> <metrics methods="0" coveredmethods="0" statements="0" coveredstatements="0" elements="0" coveredelements="0"/> </class> <class name="BankAccount"> <metrics methods="4" coveredmethods="4" statements="13" coveredstatements="5" elements="17" coveredelements="9"/> </class> <line num="77" type="method" count="3"/> <line num="79" type="stmt" count="3"/> <line num="89" type="method" count="2"/> <line num="91" type="stmt" count="2"/> <line num="92" type="stmt" count="0"/> <line num="93" type="stmt" count="0"/> <line num="94" type="stmt" count="2"/> <line num="96" type="stmt" count="0"/> <line num="105" type="method" count="1"/> <line num="107" type="stmt" count="1"/> <line num="109" type="stmt" count="0"/> <line num="119" type="method" count="1"/> <line num="121" type="stmt" count="1"/> <line num="123" type="stmt" count="0"/> <metrics loc="126" ncloc="37" classes="2" methods="4" coveredmethods="4" statements="13" coveredstatements="5" elements="17" coveredelements="9"/> </file> <metrics files="1" loc="126" ncloc="37" classes="2" methods="4" coveredmethods="4" statements="13" coveredstatements="5" elements="17" coveredelements="9"/> </project> </coverage>
以易于常人了解(human-readable)的格式生成代码覆盖率,输出到命令行或保存成文本文件。这个输出格式旨在为工做于少许类时提供快捷的覆盖状况概览。对于更大的项目,这个输出有助于对项目的覆盖状况有一个快速的概览,或者配合 --filter
功能使用也会颇有用。若从命令行调用而且写入到 php://stdout
,--colors
设置会很是好用。从命令行调用时,写入到标准输出是默认选项。默认状况下,只会显示至少有一行被覆盖的文件。这只能经过 XML 配置选项 showUncoveredFiles
来改变。参见 “Logging (日志记录)”一节。默认状况下,在详细报告中会显示全部文件以及它们的覆盖状况。这能够经过 XML 配置选项 showOnlySummary
来改变。.
能够用多种方式对 PHPUnit 进行扩展,使编写测试更容易,以及对运行测试所获得的反馈进行定制。扩展 PHPUnit 时,通常从这些点入手:
编写自定义断言时,最佳实践是遵循 PHPUnit 自有断言的实现方式。正如 例 14.1中所示,assertTrue()
方法只是对 isTrue()
和 assertThat()
方法的外包覆:isTrue()
建立了一个匹配器对象,将其传递给 assertThat()
进行评定。
例 14.1: PHPUnit_Framework_Assert 类的 assertTrue() 与 isTrue() 方法
<?php abstract class PHPUnit_Framework_Assert { // ... /** * 断言某个条件为真。 * * @param boolean $condition * @param string $message * @throws PHPUnit_Framework_AssertionFailedError */ public static function assertTrue($condition, $message = '') { self::assertThat($condition, self::isTrue(), $message); } // ... /** * 返回一个 PHPUnit_Framework_Constraint_IsTrue 匹配器对象 * * @return PHPUnit_Framework_Constraint_IsTrue * @since Method available since Release 3.3.0 */ public static function isTrue() { return new PHPUnit_Framework_Constraint_IsTrue; } // ... }?>
例 14.2展现了 PHPUnit_Framework_Constraint_IsTrue
是如何扩展针对匹配器对象(或约束)的抽象基类 PHPUnit_Framework_Constraint
的。
例 14.2: PHPUnit_Framework_Constraint_IsTrue 类
<?php class PHPUnit_Framework_Constraint_IsTrue extends PHPUnit_Framework_Constraint { /** * 对参数 $other 进行约束评定。若是符合约束, * 返回 TRUE,不然返回 FALSE。 * * @param mixed $other Value or object to evaluate. * @return bool */ public function matches($other) { return $other === TRUE; } /** * 返回表明此约束的字符串。 * * @return string */ public function toString() { return 'is true'; } }?>
在实现 assertTrue()
和 isTrue()
方法及 PHPUnit_Framework_Constraint_IsTrue
类时所付出的努力带来了一些好处,assertThat()
可以自动负责起断言的评定与任务簿记(例如为了统计目的而对其进行计数)工做。此外, isTrue()
方法还能够在配置仿件对象时用来做为匹配器。
例 14.3展现了 PHPUnit_Framework_TestListener
接口的一个简单实现。
例 14.3: 简单的测试监听器
<?php class SimpleTestListener implements PHPUnit_Framework_TestListener { public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { printf("Error while running test '%s'.\n", $test->getName()); } public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { printf("Test '%s' failed.\n", $test->getName()); } public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { printf("Test '%s' is incomplete.\n", $test->getName()); } public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { printf("Test '%s' is deemed risky.\n", $test->getName()); } public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { printf("Test '%s' has been skipped.\n", $test->getName()); } public function startTest(PHPUnit_Framework_Test $test) { printf("Test '%s' started.\n", $test->getName()); } public function endTest(PHPUnit_Framework_Test $test, $time) { printf("Test '%s' ended.\n", $test->getName()); } public function startTestSuite(PHPUnit_Framework_TestSuite $suite) { printf("TestSuite '%s' started.\n", $suite->getName()); } public function endTestSuite(PHPUnit_Framework_TestSuite $suite) { printf("TestSuite '%s' ended.\n", $suite->getName()); } } ?>
例 14.4展现了如何从抽象类 PHPUnit_Framework_BaseTestListener
派生子类,这个抽象类为全部接口方法提供了空白实现,这样你就只须要指定那些在你的使用情境下有意义的接口方法。
例 14.4: 使用测试监听器基类
<?php class ShortTestListener extends PHPUnit_Framework_BaseTestListener { public function endTest(PHPUnit_Framework_Test $test, $time) { printf("Test '%s' ended.\n", $test->getName()); } } ?>
在“测试监听器”一节中能够看到如何配置 PHPUnit 来将测试监听器附加到测试执行过程上。
能够将测试用例或者测试套件包装在 PHPUnit_Extensions_TestDecorator
的子类中并运用 Decorator(修饰器)设计模式来在测试运行先后执行一些动做。
PHPUnit 了包含了一个具体的测试修饰器:PHPUnit_Extensions_RepeatedTest
。它用于重复运行某个测试,而且只在所有循环中都成功时计为成功。
例 14.5展现了测试修饰器 PHPUnit_Extensions_RepeatedTest
的一个删减版本,用以说明如何编写你本身的测试修饰器。
例 14.5: RepeatedTest 修饰器
<?php require_once 'PHPUnit/Extensions/TestDecorator.php'; class PHPUnit_Extensions_RepeatedTest extends PHPUnit_Extensions_TestDecorator { private $timesRepeat = 1; public function __construct(PHPUnit_Framework_Test $test, $timesRepeat = 1) { parent::__construct($test); if (is_integer($timesRepeat) && $timesRepeat >= 0) { $this->timesRepeat = $timesRepeat; } } public function count() { return $this->timesRepeat * $this->test->count(); } public function run(PHPUnit_Framework_TestResult $result = NULL) { if ($result === NULL) { $result = $this->createResult(); } for ($i = 0; $i < $this->timesRepeat && !$result->shouldStop(); $i++) { $this->test->run($result); } return $result; } } ?>
PHPUnit_Framework_Test
接口是比较狭义的,十分容易实现。举例来讲,你能够自行为 PHPUnit_Framework_Test
编写一个相似于 PHPUnit_Framework_TestCase
的实现来运行数据驱动测试。
例 14.6展现了一个数据驱动的测试用例类,对来自 CSV 文件内的值进行比较。这个文件内的每一个行看起来相似于 foo;bar
,第一个值是指望值,第二个值则是实际值。
例 14.6: 一个数据驱动的测试
<?php class DataDrivenTest implements PHPUnit_Framework_Test { private $lines; public function __construct($dataFile) { $this->lines = file($dataFile); } public function count() { return 1; } public function run(PHPUnit_Framework_TestResult $result = NULL) { if ($result === NULL) { $result = new PHPUnit_Framework_TestResult; } foreach ($this->lines as $line) { $result->startTest($this); PHP_Timer::start(); $stopTime = NULL; list($expected, $actual) = explode(';', $line); try { PHPUnit_Framework_Assert::assertEquals( trim($expected), trim($actual) ); } catch (PHPUnit_Framework_AssertionFailedError $e) { $stopTime = PHP_Timer::stop(); $result->addFailure($this, $e, $stopTime); } catch (Exception $e) { $stopTime = PHP_Timer::stop(); $result->addError($this, $e, $stopTime); } if ($stopTime === NULL) { $stopTime = PHP_Timer::stop(); } $result->endTest($this, $stopTime); } return $result; } } $test = new DataDrivenTest('data_file.csv'); $result = PHPUnit_TextUI_TestRunner::run($test); ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. .F Time: 0 seconds There was 1 failure: 1) DataDrivenTest Failed asserting that two strings are equal. expected string <bar> difference < x> got string <baz> /home/sb/DataDrivenTest.php:32 /home/sb/DataDrivenTest.php:53 FAILURES! Tests: 2, Failures: 1.
本附录列举可用的各类断言方法。
assertArrayHasKey(mixed $key, array $array[, string $message = ''])
当 $array
不包含 $key
时报告错误,错误讯息由 $message
指定。
assertArrayNotHasKey()
是与之相反的断言,接受相同的参数。
例 A.1: assertArrayHasKey() 的用法
<?php class ArrayHasKeyTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertArrayHasKey('foo', array('bar' => 'baz')); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) ArrayHasKeyTest::testFailure Failed asserting that an array has the key 'foo'. /home/sb/ArrayHasKeyTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit ArrayHasKeyTest
assertClassHasAttribute(string $attributeName, string $className[, string $message = ''])
当 $className::attributeName
不存在时报告错误,错误讯息由 $message
指定。
assertClassNotHasAttribute()
是与之相反的断言,接受相同的参数。
例 A.2: assertClassHasAttribute() 的用法
<?php class ClassHasAttributeTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertClassHasAttribute('foo', 'stdClass'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 4.75Mb There was 1 failure: 1) ClassHasAttributeTest::testFailure Failed asserting that class "stdClass" has attribute "foo". /home/sb/ClassHasAttributeTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit ClassHasAttributeTest
assertArraySubset(array $subset, array $array[, bool $strict = '', string $message = ''])
当 $array
不包含 $subset
时报告错误,错误讯息由 $message
指定。
$strict
是一个标志,用于代表是否须要对数组中的对象进行全等断定。
例 A.3: assertArraySubset() 的用法
<?php class ArraySubsetTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertArraySubset(['config' => ['key-a', 'key-b']], ['config' => ['key-a']]); } } ?>
PHPUnit 4.4.0 by Sebastian Bergmann. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) Epilog\EpilogTest::testNoFollowOption Failed asserting that an array has the subset Array &0 ( 'config' => Array &1 ( 0 => 'key-a' 1 => 'key-b' ) ). /home/sb/ArraySubsetTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit ArrayHasKeyTest
assertClassHasStaticAttribute(string $attributeName, string $className[, string $message = ''])
当 $className::attributeName
不存在时报告错误,错误讯息由 $message
指定。
assertClassNotHasStaticAttribute()
是与之相反的断言,接受相同的参数。
例 A.4: assertClassHasStaticAttribute() 的用法
<?php class ClassHasStaticAttributeTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertClassHasStaticAttribute('foo', 'stdClass'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 4.75Mb There was 1 failure: 1) ClassHasStaticAttributeTest::testFailure Failed asserting that class "stdClass" has static attribute "foo". /home/sb/ClassHasStaticAttributeTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit ClassHasStaticAttributeTest
assertContains(mixed $needle, Iterator|array $haystack[, string $message = ''])
当 $needle
不是 $haystack
的元素时报告错误,错误讯息由 $message
指定。
assertNotContains()
是与之相反的断言,接受相同的参数。
assertAttributeContains()
和 assertAttributeNotContains()
是便捷包装(convenience wrapper),以某个类或对象的 public
、protected
或 private
属性为搜索范围。
例 A.5: assertContains() 的用法
<?php class ContainsTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertContains(4, array(1, 2, 3)); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) ContainsTest::testFailure Failed asserting that an array contains 4. /home/sb/ContainsTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit ContainsTest
assertContains(string $needle, string $haystack[, string $message = '', boolean $ignoreCase = FALSE])
当 $needle
不是 $haystack
的子字符串时报告错误,错误讯息由 $message
指定。
若是 $ignoreCase
为 TRUE
,测试将按大小写不敏感的方式进行。
例 A.6: assertContains() 的用法
<?php class ContainsTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertContains('baz', 'foobar'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) ContainsTest::testFailure Failed asserting that 'foobar' contains "baz". /home/sb/ContainsTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit ContainsTest
例 A.7: 带有 $ignoreCase 参数的 assertContains() 的用法
<?php class ContainsTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertContains('foo', 'FooBar'); } public function testOK() { $this->assertContains('foo', 'FooBar', '', true); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F. Time: 0 seconds, Memory: 2.75Mb There was 1 failure: 1) ContainsTest::testFailure Failed asserting that 'FooBar' contains "foo". /home/sb/ContainsTest.php:6 FAILURES! Tests: 2, Assertions: 2, Failures: 1.phpunit ContainsTest
assertContainsOnly(string $type, Iterator|array $haystack[, boolean $isNativeType = NULL, string $message = ''])
当 $haystack
并不是仅包含类型为 $type
的变量时报告错误,错误讯息由 $message
指定。
$isNativeType
是一个标志,用来代表 $type
是不是原生 PHP 类型。
assertNotContainsOnly()
是与之相反的断言,并接受相同的参数。
assertAttributeContainsOnly()
和 assertAttributeNotContainsOnly()
是便捷包装(convenience wrapper),以某个类或对象的 public
、protected
或 private
属性为搜索范围。
例 A.8: assertContainsOnly() 的用法
<?php class ContainsOnlyTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertContainsOnly('string', array('1', '2', 3)); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) ContainsOnlyTest::testFailure Failed asserting that Array ( 0 => '1' 1 => '2' 2 => 3 ) contains only values of type "string". /home/sb/ContainsOnlyTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit ContainsOnlyTest
assertContainsOnlyInstancesOf(string $classname, Traversable|array $haystack[, string $message = ''])
当 $haystack
并不是仅包含类 $classname
的实例时报告错误,错误讯息由 $message
指定。
例 A.9: assertContainsOnlyInstancesOf() 的用法
<?php class ContainsOnlyInstancesOfTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertContainsOnlyInstancesOf('Foo', array(new Foo(), new Bar(), new Foo())); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) ContainsOnlyInstancesOfTest::testFailure Failed asserting that Array ([0]=> Bar Object(...)) is an instance of class "Foo". /home/sb/ContainsOnlyInstancesOfTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit ContainsOnlyInstancesOfTest
assertCount($expectedCount, $haystack[, string $message = ''])
当 $haystack
中的元素数量不是 $expectedCount
时报告错误,错误讯息由 $message
指定。
assertNotCount()
是与之相反的断言,接受相同的参数。
例 A.10: assertCount() 的用法
<?php class CountTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertCount(0, array('foo')); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 4.75Mb There was 1 failure: 1) CountTest::testFailure Failed asserting that actual size 1 matches expected size 0. /home/sb/CountTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit CountTest
assertEmpty(mixed $actual[, string $message = ''])
当 $actual
非空时报告错误,错误讯息由 $message
指定。
assertNotEmpty()
是与之相反的断言,接受相同的参数。
assertAttributeEmpty()
和 assertAttributeNotEmpty()
是便捷包装(convenience wrapper),能够应用于某个类或对象的某个 public
、protected
或 private
属性。
例 A.11: assertEmpty() 的用法
<?php class EmptyTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertEmpty(array('foo')); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 4.75Mb There was 1 failure: 1) EmptyTest::testFailure Failed asserting that an array is empty. /home/sb/EmptyTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit EmptyTest
assertEqualXMLStructure(DOMElement $expectedElement, DOMElement $actualElement[, boolean $checkAttributes = FALSE, string $message = ''])
当 $actualElement
中 DOMElement 的 XML 结构与 $expectedElement
中 DOMElement的 XML 结构不相同时报告错误,错误讯息由 $message
指定。
例 A.12: assertEqualXMLStructure() 的用法
<?php class EqualXMLStructureTest extends PHPUnit_Framework_TestCase { public function testFailureWithDifferentNodeNames() { $expected = new DOMElement('foo'); $actual = new DOMElement('bar'); $this->assertEqualXMLStructure($expected, $actual); } public function testFailureWithDifferentNodeAttributes() { $expected = new DOMDocument; $expected->loadXML('<foo bar="true" />'); $actual = new DOMDocument; $actual->loadXML('<foo/>'); $this->assertEqualXMLStructure( $expected->firstChild, $actual->firstChild, TRUE ); } public function testFailureWithDifferentChildrenCount() { $expected = new DOMDocument; $expected->loadXML('<foo><bar/><bar/><bar/></foo>'); $actual = new DOMDocument; $actual->loadXML('<foo><bar/></foo>'); $this->assertEqualXMLStructure( $expected->firstChild, $actual->firstChild ); } public function testFailureWithDifferentChildren() { $expected = new DOMDocument; $expected->loadXML('<foo><bar/><bar/><bar/></foo>'); $actual = new DOMDocument; $actual->loadXML('<foo><baz/><baz/><baz/></foo>'); $this->assertEqualXMLStructure( $expected->firstChild, $actual->firstChild ); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. FFFF Time: 0 seconds, Memory: 5.75Mb There were 4 failures: 1) EqualXMLStructureTest::testFailureWithDifferentNodeNames Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'foo' +'bar' /home/sb/EqualXMLStructureTest.php:9 2) EqualXMLStructureTest::testFailureWithDifferentNodeAttributes Number of attributes on node "foo" does not match Failed asserting that 0 matches expected 1. /home/sb/EqualXMLStructureTest.php:22 3) EqualXMLStructureTest::testFailureWithDifferentChildrenCount Number of child nodes of "foo" differs Failed asserting that 1 matches expected 3. /home/sb/EqualXMLStructureTest.php:35 4) EqualXMLStructureTest::testFailureWithDifferentChildren Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'bar' +'baz' /home/sb/EqualXMLStructureTest.php:48 FAILURES! Tests: 4, Assertions: 8, Failures: 4.phpunit EqualXMLStructureTest
assertEquals(mixed $expected, mixed $actual[, string $message = ''])
当两个变量 $expected
和 $actual
不相等时报告错误,错误讯息由 $message
指定。
assertNotEquals()
是与之相反的断言,接受相同的参数。
assertAttributeEquals()
和 assertAttributeNotEquals()
是便捷包装(convenience wrapper),以某个类或对象的某个 public
、protected
或 private
属性做为实际值来进行比较。
例 A.13: assertEquals() 的用法
<?php class EqualsTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertEquals(1, 0); } public function testFailure2() { $this->assertEquals('bar', 'baz'); } public function testFailure3() { $this->assertEquals("foo\nbar\nbaz\n", "foo\nbah\nbaz\n"); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. FFF Time: 0 seconds, Memory: 5.25Mb There were 3 failures: 1) EqualsTest::testFailure Failed asserting that 0 matches expected 1. /home/sb/EqualsTest.php:6 2) EqualsTest::testFailure2 Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'bar' +'baz' /home/sb/EqualsTest.php:11 3) EqualsTest::testFailure3 Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ 'foo -bar +bah baz ' /home/sb/EqualsTest.php:16 FAILURES! Tests: 3, Assertions: 3, Failures: 3.phpunit EqualsTest
若是 $expected
和 $actual
是某些特定的类型,将使用更加专门的比较方式,参阅下文。
assertEquals(float $expected, float $actual[, string $message = '', float $delta = 0])
当两个浮点数 $expected
和 $actual
之间的差值(的绝对值)大于 $delta
时报告错误,错误讯息由 $message
指定。
关于为何 $delta
参数是必须的,请阅读《关于浮点运算,每一位计算机科学从业人员都应该知道的事实》。
例 A.14: 将assertEquals()用于浮点数时的用法
<?php class EqualsTest extends PHPUnit_Framework_TestCase { public function testSuccess() { $this->assertEquals(1.0, 1.1, '', 0.2); } public function testFailure() { $this->assertEquals(1.0, 1.1); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. .F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) EqualsTest::testFailure Failed asserting that 1.1 matches expected 1.0. /home/sb/EqualsTest.php:11 FAILURES! Tests: 2, Assertions: 2, Failures: 1.phpunit EqualsTest
assertEquals(DOMDocument $expected, DOMDocument $actual[, string $message = ''])
当 $expected
和 $actual
这两个 DOMDocument 对象所表示的 XML 文档对应的无注释规范形式不相同时报告错误,错误讯息由 $message
指定。
例 A.15: assertEquals()应用于 DOMDocument 对象时的用法
<?php class EqualsTest extends PHPUnit_Framework_TestCase { public function testFailure() { $expected = new DOMDocument; $expected->loadXML('<foo><bar/></foo>'); $actual = new DOMDocument; $actual->loadXML('<bar><foo/></bar>'); $this->assertEquals($expected, $actual); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) EqualsTest::testFailure Failed asserting that two DOM documents are equal. --- Expected +++ Actual @@ @@ <?xml version="1.0"?> -<foo> - <bar/> -</foo> +<bar> + <foo/> +</bar> /home/sb/EqualsTest.php:12 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit EqualsTest
assertEquals(object $expected, object $actual[, string $message = ''])
当 $expected
和 $actual
这两个对象的属性值不相等时报告错误,错误讯息由 $message
指定。
例 A.16: assertEquals()应用于对象时的用法
<?php class EqualsTest extends PHPUnit_Framework_TestCase { public function testFailure() { $expected = new stdClass; $expected->foo = 'foo'; $expected->bar = 'bar'; $actual = new stdClass; $actual->foo = 'bar'; $actual->baz = 'bar'; $this->assertEquals($expected, $actual); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) EqualsTest::testFailure Failed asserting that two objects are equal. --- Expected +++ Actual @@ @@ stdClass Object ( - 'foo' => 'foo' - 'bar' => 'bar' + 'foo' => 'bar' + 'baz' => 'bar' ) /home/sb/EqualsTest.php:14 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit EqualsTest
assertEquals(array $expected, array $actual[, string $message = ''])
当 $expected
和 $actual
这两个数组不相等时报告错误,错误讯息由 $message
指定。
例 A.17: assertEquals() 应用于数组时的用法
<?php class EqualsTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertEquals(array('a', 'b', 'c'), array('a', 'c', 'd')); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) EqualsTest::testFailure Failed asserting that two arrays are equal. --- Expected +++ Actual @@ @@ Array ( 0 => 'a' - 1 => 'b' - 2 => 'c' + 1 => 'c' + 2 => 'd' ) /home/sb/EqualsTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit EqualsTest
assertFalse(bool $condition[, string $message = ''])
当 $condition
为 TRUE
时报告错误,错误讯息由 $message
指定。
assertNotFalse()
是与之相反的断言,接受相同的参数。
例 A.18: assertFalse() 的用法
<?php class FalseTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertFalse(TRUE); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) FalseTest::testFailure Failed asserting that true is false. /home/sb/FalseTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit FalseTest
assertFileEquals(string $expected, string $actual[, string $message = ''])
当 $expected
所指定的文件与 $actual
所指定的文件内容不一样时报告错误,错误讯息由 $message
指定。
assertFileNotEquals()
是与之相反的断言,接受相同的参数。
例 A.19: assertFileEquals() 的用法
<?php class FileEqualsTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertFileEquals('/home/sb/expected', '/home/sb/actual'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) FileEqualsTest::testFailure Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'expected +'actual ' /home/sb/FileEqualsTest.php:6 FAILURES! Tests: 1, Assertions: 3, Failures: 1.phpunit FileEqualsTest
assertFileExists(string $filename[, string $message = ''])
当 $filename
所指定的文件不存在时报告错误,错误讯息由 $message
指定。
assertFileNotExists()
是与之相反的断言,接受相同的参数。
例 A.20: assertFileExists() 的用法
<?php class FileExistsTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertFileExists('/path/to/file'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 4.75Mb There was 1 failure: 1) FileExistsTest::testFailure Failed asserting that file "/path/to/file" exists. /home/sb/FileExistsTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit FileExistsTest
assertGreaterThan(mixed $expected, mixed $actual[, string $message = ''])
当 $actual
的值不大于 $expected
的值时报告错误,错误讯息由 $message
指定。
assertAttributeGreaterThan()
是便捷包装(convenience wrapper),以某个类或对象的某个 public
、protected
或 private
属性做为实际值来进行比较。
例 A.21: assertGreaterThan() 的用法
<?php class GreaterThanTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertGreaterThan(2, 1); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) GreaterThanTest::testFailure Failed asserting that 1 is greater than 2. /home/sb/GreaterThanTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit GreaterThanTest
assertGreaterThanOrEqual(mixed $expected, mixed $actual[, string $message = ''])
当 $actual
的值不大于且不等于 $expected
的值时报告错误,错误讯息由 $message
指定。
assertAttributeGreaterThanOrEqual()
是便捷包装(convenience wrapper),以某个类或对象的某个 public
、protected
或 private
属性做为实际值来进行比较。
例 A.22: assertGreaterThanOrEqual() 的用法
<?php class GreatThanOrEqualTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertGreaterThanOrEqual(2, 1); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) GreatThanOrEqualTest::testFailure Failed asserting that 1 is equal to 2 or is greater than 2. /home/sb/GreaterThanOrEqualTest.php:6 FAILURES! Tests: 1, Assertions: 2, Failures: 1.phpunit GreaterThanOrEqualTest
assertInfinite(mixed $variable[, string $message = ''])
当 $actual
不是 INF
时报告错误,错误讯息由 $message
指定。
assertFinite()
是与之相反的断言,接受相同的参数。
例 A.23: assertInfinite() 的用法
<?php class InfiniteTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertInfinite(1); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) InfiniteTest::testFailure Failed asserting that 1 is infinite. /home/sb/InfiniteTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit InfiniteTest
assertInstanceOf($expected, $actual[, $message = ''])
当 $actual
不是 $expected
的实例时报告错误,错误讯息由 $message
指定。
assertNotInstanceOf()
是与之相反的断言,接受相同的参数。
assertAttributeInstanceOf()
和 assertAttributeNotInstanceOf()
是便捷包装(convenience wrapper),能够应用于某个类或对象的某个 public
、protected
或 private
属性。
例 A.24: assertInstanceOf() 的用法
<?php class InstanceOfTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertInstanceOf('RuntimeException', new Exception); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) InstanceOfTest::testFailure Failed asserting that Exception Object (...) is an instance of class "RuntimeException". /home/sb/InstanceOfTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit InstanceOfTest
assertInternalType($expected, $actual[, $message = ''])
当 $actual
不是 $expected
所指明的类型时报告错误,错误讯息由 $message
指定。
assertNotInternalType()
是与之相反的断言,接受相同的参数。
assertAttributeInternalType()
和 assertAttributeNotInternalType()
是便捷包装(convenience wrapper),能够应用于某个类或对象的某个 public
、protected
或 private
属性。
例 A.25: assertInternalType() 的用法
<?php class InternalTypeTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertInternalType('string', 42); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) InternalTypeTest::testFailure Failed asserting that 42 is of type "string". /home/sb/InternalTypeTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit InternalTypeTest
assertJsonFileEqualsJsonFile(mixed $expectedFile, mixed $actualFile[, string $message = ''])
当 $actualFile
对应的值与 $expectedFile
对应的值不匹配时报告错误,错误讯息由 $message
指定。
例 A.26: assertJsonFileEqualsJsonFile() 的用法
<?php class JsonFileEqualsJsonFileTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertJsonFileEqualsJsonFile( 'path/to/fixture/file', 'path/to/actual/file'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) JsonFileEqualsJsonFile::testFailure Failed asserting that '{"Mascott":"Tux"}' matches JSON string "["Mascott", "Tux", "OS", "Linux"]". /home/sb/JsonFileEqualsJsonFileTest.php:5 FAILURES! Tests: 1, Assertions: 3, Failures: 1.phpunit JsonFileEqualsJsonFileTest
assertJsonStringEqualsJsonFile(mixed $expectedFile, mixed $actualJson[, string $message = ''])
当 $actualJson
对应的值与 $expectedFile
对应的值不匹配时报告错误,错误讯息由 $message
指定。
例 A.27: assertJsonStringEqualsJsonFile() 的用法
<?php class JsonStringEqualsJsonFileTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertJsonStringEqualsJsonFile( 'path/to/fixture/file', json_encode(array("Mascott" => "ux")) ); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) JsonStringEqualsJsonFile::testFailure Failed asserting that '{"Mascott":"ux"}' matches JSON string "{"Mascott":"Tux"}". /home/sb/JsonStringEqualsJsonFileTest.php:5 FAILURES! Tests: 1, Assertions: 3, Failures: 1.phpunit JsonStringEqualsJsonFileTest
assertJsonStringEqualsJsonString(mixed $expectedJson, mixed $actualJson[, string $message = ''])
当 $actualJson
对应的值与 $expectedJson
对应的值不匹配时报告错误,错误讯息由 $message
指定。
例 A.28: assertJsonStringEqualsJsonString() 的用法
<?php class JsonStringEqualsJsonStringTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertJsonStringEqualsJsonString( json_encode(array("Mascott" => "Tux")), json_encode(array("Mascott" => "ux")) ); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) JsonStringEqualsJsonStringTest::testFailure Failed asserting that two objects are equal. --- Expected +++ Actual @@ @@ stdClass Object ( - 'Mascott' => 'Tux' + 'Mascott' => 'ux' ) /home/sb/JsonStringEqualsJsonStringTest.php:5 FAILURES! Tests: 1, Assertions: 3, Failures: 1.phpunit JsonStringEqualsJsonStringTest
assertLessThan(mixed $expected, mixed $actual[, string $message = ''])
当 $actual
的值不小于 $expected
的值时报告错误,错误讯息由 $message
指定。
assertAttributeLessThan()
是便捷包装(convenience wrapper),以某个类或对象的某个 public
、protected
或 private
属性做为实际值来进行比较。
例 A.29: assertLessThan() 的用法
<?php class LessThanTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertLessThan(1, 2); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) LessThanTest::testFailure Failed asserting that 2 is less than 1. /home/sb/LessThanTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit LessThanTest
assertLessThanOrEqual(mixed $expected, mixed $actual[, string $message = ''])
当 $actual
的值不小于且不等于 $expected
的值时报告错误,错误讯息由 $message
指定。
assertAttributeLessThanOrEqual()
是便捷包装(convenience wrapper),以某个类或对象的某个 public
、protected
或 private
属性做为实际值来进行比较。
例 A.30: assertLessThanOrEqual() 的用法
<?php class LessThanOrEqualTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertLessThanOrEqual(1, 2); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) LessThanOrEqualTest::testFailure Failed asserting that 2 is equal to 1 or is less than 1. /home/sb/LessThanOrEqualTest.php:6 FAILURES! Tests: 1, Assertions: 2, Failures: 1.phpunit LessThanOrEqualTest
assertNan(mixed $variable[, string $message = ''])
当 $variable
不是 NAN
时报告错误,错误讯息由 $message
指定。
例 A.31: assertNan() 的用法
<?php class NanTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertNan(1); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) NanTest::testFailure Failed asserting that 1 is nan. /home/sb/NanTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit NanTest
assertNull(mixed $variable[, string $message = ''])
当 $actual
不是 NULL
时报告错误,错误讯息由 $message
指定。
assertNotNull()
是与之相反的断言,接受相同的参数。
例 A.32: assertNull() 的使用
<?php class NullTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertNull('foo'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) NullTest::testFailure Failed asserting that 'foo' is null. /home/sb/NotNullTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit NotNullTest
assertObjectHasAttribute(string $attributeName, object $object[, string $message = ''])
当 $object->attributeName
不存在时报告错误,错误讯息由 $message
指定。
assertObjectNotHasAttribute()
是与之相反的断言,接受相同的参数。
例 A.33: assertObjectHasAttribute() 的用法
<?php class ObjectHasAttributeTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertObjectHasAttribute('foo', new stdClass); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 4.75Mb There was 1 failure: 1) ObjectHasAttributeTest::testFailure Failed asserting that object of class "stdClass" has attribute "foo". /home/sb/ObjectHasAttributeTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit ObjectHasAttributeTest
assertRegExp(string $pattern, string $string[, string $message = ''])
当 $string
不匹配于正则表达式 $pattern
时报告错误,错误讯息由 $message
指定。
assertNotRegExp()
是与之相反的断言,接受相同的参数。
例 A.34: assertRegExp() 的用法
<?php class RegExpTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertRegExp('/foo/', 'bar'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) RegExpTest::testFailure Failed asserting that 'bar' matches PCRE pattern "/foo/". /home/sb/RegExpTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit RegExpTest
assertStringMatchesFormat(string $format, string $string[, string $message = ''])
当 $string
不匹配于 $format
定义的格式时报告错误,错误讯息由 $message
指定。
assertStringNotMatchesFormat()
是与之相反的断言,接受相同的参数。
例 A.35: assertStringMatchesFormat() 的用法
<?php class StringMatchesFormatTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertStringMatchesFormat('%i', 'foo'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) StringMatchesFormatTest::testFailure Failed asserting that 'foo' matches PCRE pattern "/^[+-]?\d+$/s". /home/sb/StringMatchesFormatTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit StringMatchesFormatTest
格式定义字符串中能够使用以下占位符:
%e
:表示目录分隔符,例如在 Linux 系统中是 /
。
%s
:一个或多个除了换行符之外的任意字符(非空白字符或者空白字符)。
%S
:零个或多个除了换行符之外的任意字符(非空白字符或者空白字符)。
%a
:一个或多个包括换行符在内的任意字符(非空白字符或者空白字符)。
%A
:零个或多个包括换行符在内的任意字符(非空白字符或者空白字符)。
%w
:零个或多个空白字符。
%i
:带符号整数值,例如 +3142
、-3142
。
%d
:无符号整数值,例如 123456
。
%x
:一个或多个十六进制字符。所谓十六进制字符,指的是在如下范围内的字符:0-9
、a-f
、A-F
。
%f
:浮点数,例如 3.142
、-3.142
、3.142E-10
、3.142e+10
。
%c
:单个任意字符。
assertStringMatchesFormatFile(string $formatFile, string $string[, string $message = ''])
当 $string
不匹配于 $formatFile
的内容所定义的格式时报告错误,错误讯息由 $message
指定。
assertStringNotMatchesFormatFile()
是与之相反的断言,接受相同的参数。
例 A.36: assertStringMatchesFormatFile() 的用法
<?php class StringMatchesFormatFileTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertStringMatchesFormatFile('/path/to/expected.txt', 'foo'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) StringMatchesFormatFileTest::testFailure Failed asserting that 'foo' matches PCRE pattern "/^[+-]?\d+ $/s". /home/sb/StringMatchesFormatFileTest.php:6 FAILURES! Tests: 1, Assertions: 2, Failures: 1.phpunit StringMatchesFormatFileTest
assertSame(mixed $expected, mixed $actual[, string $message = ''])
当两个变量 $expected
和 $actual
的值与类型不彻底相同时报告错误,错误讯息由 $message
指定。
assertNotSame()
是与之相反的断言,接受相同的参数。
assertAttributeSame()
和 assertAttributeNotSame()
是便捷包装(convenience wrapper),以某个类或对象的某个 public
、protected
或 private
属性做为实际值来进行比较。
例 A.37: assertSame() 的用法
<?php class SameTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertSame('2204', 2204); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) SameTest::testFailure Failed asserting that 2204 is identical to '2204'. /home/sb/SameTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit SameTest
assertSame(object $expected, object $actual[, string $message = ''])
当两个变量 $expected
和 $actual
不是指向同一个对象的引用时报告错误,错误讯息由 $message
指定。
例 A.38: assertSame() 应用于对象时的用法
<?php class SameTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertSame(new stdClass, new stdClass); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 4.75Mb There was 1 failure: 1) SameTest::testFailure Failed asserting that two variables reference the same object. /home/sb/SameTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit SameTest
assertStringEndsWith(string $suffix, string $string[, string $message = ''])
当 $string
不以 $suffix
结尾时报告错误,错误讯息由 $message
指定。
assertStringEndsNotWith()
是与之相反的断言,接受相同的参数。
例 A.39: assertStringEndsWith() 的用法
<?php class StringEndsWithTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertStringEndsWith('suffix', 'foo'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 1 second, Memory: 5.00Mb There was 1 failure: 1) StringEndsWithTest::testFailure Failed asserting that 'foo' ends with "suffix". /home/sb/StringEndsWithTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit StringEndsWithTest
assertStringEqualsFile(string $expectedFile, string $actualString[, string $message = ''])
当 $expectedFile
所指定的文件其内容不是 $actualString
时报告错误,错误讯息由 $message
指定。
assertStringNotEqualsFile()
是与之相反的断言,接受相同的参数。
例 A.40: assertStringEqualsFile() 的用法
<?php class StringEqualsFileTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertStringEqualsFile('/home/sb/expected', 'actual'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) StringEqualsFileTest::testFailure Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'expected -' +'actual' /home/sb/StringEqualsFileTest.php:6 FAILURES! Tests: 1, Assertions: 2, Failures: 1.phpunit StringEqualsFileTest
assertStringStartsWith(string $prefix, string $string[, string $message = ''])
当 $string
不以 $prefix
开头时报告错误,错误讯息由 $message
指定。
assertStringStartsNotWith()
是与之相反的断言,并接受相同的参数。
例 A.41: assertStringStartsWith() 的用法
<?php class StringStartsWithTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertStringStartsWith('prefix', 'foo'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) StringStartsWithTest::testFailure Failed asserting that 'foo' starts with "prefix". /home/sb/StringStartsWithTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit StringStartsWithTest
能够用 PHPUnit_Framework_Constraint
类来订立更加复杂的断言。随后能够用 assertThat()
方法来评定这些断言。例 A.42 展现了如何用 logicalNot()
和 equalTo()
约束条件来表达与 assertNotEquals()
等价的断言。
assertThat(mixed $value, PHPUnit_Framework_Constraint $constraint[, $message = ''])
当 $value
不符合约束条件 $constraint
时报告错误,错误讯息由 $message
指定。
例 A.42: assertThat() 的用法
<?php class BiscuitTest extends PHPUnit_Framework_TestCase { public function testEquals() { $theBiscuit = new Biscuit('Ginger'); $myBiscuit = new Biscuit('Ginger'); $this->assertThat( $theBiscuit, $this->logicalNot( $this->equalTo($myBiscuit) ) ); } } ?>
表 A.1列举了全部可用的 PHPUnit_Framework_Constraint
类。
表 A.1. 约束条件
约束条件 | 含义 |
---|---|
PHPUnit_Framework_Constraint_Attribute attribute(PHPUnit_Framework_Constraint $constraint, $attributeName) |
此约束将另一个约束应用于某个类或对象的某个属性。 |
PHPUnit_Framework_Constraint_IsAnything anything() |
此约束接受任意输入值。 |
PHPUnit_Framework_Constraint_ArrayHasKey arrayHasKey(mixed $key) |
此约束断言所评定的数组拥有指定键名。 |
PHPUnit_Framework_Constraint_TraversableContains contains(mixed $value) |
此约束断言所评定的 array 或实现了 Iterator 接口的对象包含有给定值。 |
PHPUnit_Framework_Constraint_TraversableContainsOnly containsOnly(string $type) |
此约束断言所评定的 array 或实现了 Iterator 接口的对象仅包含给定类型的值。 |
PHPUnit_Framework_Constraint_TraversableContainsOnly containsOnlyInstancesOf(string $classname) |
此约束断言所评定的 array 或实现了 Iterator 接口的对象仅包含给定类名的类的实例。 |
PHPUnit_Framework_Constraint_IsEqual equalTo($value, $delta = 0, $maxDepth = 10) |
此约束检验一个值是否等于另一个。 |
PHPUnit_Framework_Constraint_Attribute attributeEqualTo($attributeName, $value, $delta = 0, $maxDepth = 10) |
此约束检验一个值是否等于某个类或对象的某个属性。 |
PHPUnit_Framework_Constraint_FileExists fileExists() |
此约束检验所评定的文件名对应的文件是否存在。 |
PHPUnit_Framework_Constraint_GreaterThan greaterThan(mixed $value) |
此约束断言所评定的值大于给定值。 |
PHPUnit_Framework_Constraint_Or greaterThanOrEqual(mixed $value) |
此约束断言所评定的值大于或等于给定值。 |
PHPUnit_Framework_Constraint_ClassHasAttribute classHasAttribute(string $attributeName) |
此约束断言所评定的类具备给定属性。 |
PHPUnit_Framework_Constraint_ClassHasStaticAttribute classHasStaticAttribute(string $attributeName) |
此约束断言所评定的类具备给定静态属性。 |
PHPUnit_Framework_Constraint_ObjectHasAttribute hasAttribute(string $attributeName) |
此约束断言所评定的对象具备给定属性。 |
PHPUnit_Framework_Constraint_IsIdentical identicalTo(mixed $value) |
此约束断言所评定的值与另一个值全等。 |
PHPUnit_Framework_Constraint_IsFalse isFalse() |
此约束断言所评定的值为 FALSE 。 |
PHPUnit_Framework_Constraint_IsInstanceOf isInstanceOf(string $className) |
此约束断言所评定的对象是给定类的实例。 |
PHPUnit_Framework_Constraint_IsNull isNull() |
此约束断言所评定的值为 NULL 。 |
PHPUnit_Framework_Constraint_IsTrue isTrue() |
此约束断言所评定的值为 TRUE 。 |
PHPUnit_Framework_Constraint_IsType isType(string $type) |
此约束断言所评定的值是指定类型的。 |
PHPUnit_Framework_Constraint_LessThan lessThan(mixed $value) |
此约束断言所评定的值小于给定值。 |
PHPUnit_Framework_Constraint_Or lessThanOrEqual(mixed $value) |
此约束断言所评定的值小于或等于给定值。 |
logicalAnd() |
逻辑与(AND)。 |
logicalNot(PHPUnit_Framework_Constraint $constraint) |
逻辑非(NOT)。 |
logicalOr() |
逻辑或(OR)。 |
logicalXor() |
逻辑异或(XOR)。 |
PHPUnit_Framework_Constraint_PCREMatch matchesRegularExpression(string $pattern) |
此约束断言所评定的字符串匹配于正则表达式。 |
PHPUnit_Framework_Constraint_StringContains stringContains(string $string, bool $case) |
此约束断言所评定的字符串包含指定字符串。 |
PHPUnit_Framework_Constraint_StringEndsWith stringEndsWith(string $suffix) |
此约束断言所评定的字符串以给定后缀结尾。 |
PHPUnit_Framework_Constraint_StringStartsWith stringStartsWith(string $prefix) |
此约束断言所评定的字符串以给定前缀开头。 |
assertTrue(bool $condition[, string $message = ''])
当 $condition
为 FALSE
时报告错误,错误讯息由 $message
指定。
assertNotTrue()
是与之相反的断言,接受相同的参数。
例 A.43: assertTrue() 的用法
<?php class TrueTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertTrue(FALSE); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) TrueTest::testFailure Failed asserting that false is true. /home/sb/TrueTest.php:6 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit TrueTest
assertXmlFileEqualsXmlFile(string $expectedFile, string $actualFile[, string $message = ''])
当 $actualFile
对应的 XML 文档与 $expectedFile
对应的 XML 文档不相同时报告错误,错误讯息由 $message
指定。
assertXmlFileNotEqualsXmlFile()
是与之相反的断言,接受相同的参数。
例 A.44: assertXmlFileEqualsXmlFile() 的用法
<?php class XmlFileEqualsXmlFileTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertXmlFileEqualsXmlFile( '/home/sb/expected.xml', '/home/sb/actual.xml'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) XmlFileEqualsXmlFileTest::testFailure Failed asserting that two DOM documents are equal. --- Expected +++ Actual @@ @@ <?xml version="1.0"?> <foo> - <bar/> + <baz/> </foo> /home/sb/XmlFileEqualsXmlFileTest.php:7 FAILURES! Tests: 1, Assertions: 3, Failures: 1.phpunit XmlFileEqualsXmlFileTest
assertXmlStringEqualsXmlFile(string $expectedFile, string $actualXml[, string $message = ''])
当 $actualXml
对应的 XML 文档与 $expectedFile
对应的 XML 文档不相同时报告错误,错误讯息由 $message
指定。
assertXmlStringNotEqualsXmlFile()
是与之相反的断言,并接受相同的参数。
例 A.45: assertXmlStringEqualsXmlFile() 的用法
<?php class XmlStringEqualsXmlFileTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertXmlStringEqualsXmlFile( '/home/sb/expected.xml', '<foo><baz/></foo>'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.25Mb There was 1 failure: 1) XmlStringEqualsXmlFileTest::testFailure Failed asserting that two DOM documents are equal. --- Expected +++ Actual @@ @@ <?xml version="1.0"?> <foo> - <bar/> + <baz/> </foo> /home/sb/XmlStringEqualsXmlFileTest.php:7 FAILURES! Tests: 1, Assertions: 2, Failures: 1.phpunit XmlStringEqualsXmlFileTest
assertXmlStringEqualsXmlString(string $expectedXml, string $actualXml[, string $message = ''])
当 $actualXml
对应的 XML 文档与 $expectedXml
对应的 XML 文档不相同时报告错误,错误讯息由 $message
指定。
assertXmlStringNotEqualsXmlString()
是与之相反的断言,接受相同的参数。
例 A.46: assertXmlStringEqualsXmlString() 的用法
<?php class XmlStringEqualsXmlStringTest extends PHPUnit_Framework_TestCase { public function testFailure() { $this->assertXmlStringEqualsXmlString( '<foo><bar/></foo>', '<foo><baz/></foo>'); } } ?>
PHPUnit 5.0.0 by Sebastian Bergmann and contributors. F Time: 0 seconds, Memory: 5.00Mb There was 1 failure: 1) XmlStringEqualsXmlStringTest::testFailure Failed asserting that two DOM documents are equal. --- Expected +++ Actual @@ @@ <?xml version="1.0"?> <foo> - <bar/> + <baz/> </foo> /home/sb/XmlStringEqualsXmlStringTest.php:7 FAILURES! Tests: 1, Assertions: 1, Failures: 1.phpunit XmlStringEqualsXmlStringTest
所谓标注,是指某些编程语言中容许加在源代码中的一种特殊格式的语法元数据。PHP 并无专门的语言特性来支持对源代码进行标注,然而 PHP 社区早已经造成惯例,经过在文档注释块中使用诸如 @annotation arguments
这样的标签来为源代码加上标注。在 PHP 中,文档注释块是可反射的:能够对函数、方法、类以及属性调用相应级别的反射 API getDocComment()
方法来获取相应的文档注释块。诸如 PHPUnit 这样的应用程序在运行时用这些信息来配置其行为。
PHP中的文档注释块必须以 /**
开头,以 */
结尾。任何其余形式的注释中出现的标注都将被忽略。
本附录列出了 PHPUnit 所支持的全部标注种类。
@author
标注是 @group
标注(参见 “@group”一节)的别名,容许基于做者对测试进行过滤。
@after
标注用于指明此方法应当在测试用例类中的每一个测试方法运行完成以后调用。
class MyTest extends PHPUnit_Framework_TestCase { /** * @after */ public function tearDownSomeFixtures() { // ... } /** * @after */ public function tearDownSomeOtherFixtures() { // ... } }
@afterClass
标注用于指明此静态方法应该于测试类中的全部测试方法都运行完成以后调用,用于清理共享基境。
class MyTest extends PHPUnit_Framework_TestCase { /** * @afterClass */ public static function tearDownSomeSharedFixtures() { // ... } /** * @afterClass */ public static function tearDownSomeOtherSharedFixtures() { // ... } }
全局变量的备份与还原操做能够对某个测试用例类中的全部测试完全禁用,像这样:
/** * @backupGlobals disabled */ class MyTest extends PHPUnit_Framework_TestCase { // ... }
@backupGlobals
标注也能够用在测试方法这一级别。这样能够对备份与还原操做进行更细粒度的配置:
/** * @backupGlobals disabled */ class MyTest extends PHPUnit_Framework_TestCase { /** * @backupGlobals enabled */ public function testThatInteractsWithGlobalVariables() { // ... } }
若是指定了 @backupStaticAttributes
标注,那么将在每一个测试以前备份全部已声明的类的静态属性的值,并在测试完成以后所有恢复。它能够用在测试用例类或测试方法级别:
/** * @backupStaticAttributes enabled */ class MyTest extends PHPUnit_Framework_TestCase { /** * @backupStaticAttributes disabled */ public function testThatInteractsWithStaticAttributes() { // ... } }
@before
标注用于指明此方法应当在测试用例类中的每一个测试方法开始运行以前调用。
class MyTest extends PHPUnit_Framework_TestCase { /** * @before */ public function setupSomeFixtures() { // ... } /** * @before */ public function setupSomeOtherFixtures() { // ... } }
@beforeClass
标注用于指明此静态方法应该于测试类中的全部测试方法都运行完成以后调用,用于创建共享基境。
class MyTest extends PHPUnit_Framework_TestCase { /** * @beforeClass */ public static function setUpSomeSharedFixtures() { // ... } /** * @beforeClass */ public static function setUpSomeOtherSharedFixtures() { // ... } }
@codeCoverageIgnore
, @codeCoverageIgnoreStart
and @codeCoverageIgnoreEnd
标注用于从覆盖率分析中排除掉某些代码行。
用法参见“略过代码块”一节。
在测试代码中用 @covers
标注来指明测试方法想要对哪些方法进行测试:
/** * @covers BankAccount::getBalance */ public function testBalanceIsInitiallyZero() { $this->assertEquals(0, $this->ba->getBalance()); }
若是提供了此标注,则代码覆盖率信息中只考虑指定的这些方法。
表 B.1列出了 @covers
标注的语法。
表 B.1. 用于指明测试覆盖哪些方法的标注
Annotation (标注) | 描述 |
---|---|
@covers ClassName::methodName |
指明所标注的测试方法覆盖指定的方法。 |
@covers ClassName |
指明所标注的测试方法覆盖给定类的所有方法。 |
@covers ClassName<extended> |
指明所标注的测试方法覆盖给定类以及其全部父类与接口的所有方法。 |
@covers ClassName::<public> |
指明所标注的测试方法覆盖给定类的全部 public 方法。 |
@covers ClassName::<protected> |
指明所标注的测试方法覆盖给定类的全部 protected 方法。 |
@covers ClassName::<private> |
指明所标注的测试方法覆盖给定类的全部 private 方法。 |
@covers ClassName::<!public> |
指明所标注的测试方法覆盖给定类的全部非 public 方法。 |
@covers ClassName::<!protected> |
指明所标注的测试方法覆盖给定类的全部非 protected 方法。 |
@covers ClassName::<!private> |
指明所标注的测试方法覆盖给定类的全部非 private 方法。 |
@covers ::functionName |
指明所标注的测试方法覆盖给定的全局函数。 |
@coversDefaultClass
标注用于指定一个默认的命名空间或类名,这样就不用在每一个 @covers
标注中重复长名称。参见例 B.1。
在测试代码中用 @coversNothing
标注来指明所标注的测试用例不须要记录任何代码覆盖率信息。
这能够用于集成测试。例子可参见例 11.3。
这个标注能够用在类级别或者方法级别,而且会覆盖掉任何 @covers
标注。
测试方法能够接受任意参数。这些参数能够由数据供给器方法(例 2.5中的 provider()
)提供。所要使用的数据供给器方法用 @dataProvider
标注来指定。
更多细节参见“数据供给器”一节。
PHPUnit支持对测试方法之间的显式依赖关系进行声明。这种依赖关系并非定义在测试方法的执行顺序中,而是容许生产者(producer)返回一个测试基境(fixture)的实例,并将此实例传递给依赖于它的消费者(consumer)们。例 2.2展现了如何用 @depends
标注来表达测试方法之间的依赖关系。
更多细节参见“测试的依赖关系”一节。
将 @expectedExceptionCode
标注与 @expectedException
联合使用,能够对抛出异常的代码做出断言,这样能够缩小具体异常的范围。
class MyTest extends PHPUnit_Framework_TestCase { /** * @expectedException MyException * @expectedExceptionCode 20 */ public function testExceptionHasErrorcode20() { throw new MyException('Some Message', 20); } }
为了方便测试并减小冗余,能够用"@expectedExceptionCode ClassName::CONST
"这样的语法将指定类常量做为 @expectedExceptionCode
class MyTest extends PHPUnit_Framework_TestCase { /** * @expectedException MyException * @expectedExceptionCode MyClass::ERRORCODE */ public function testExceptionHasErrorcode20() { throw new MyException('Some Message', 20); } } class MyClass { const ERRORCODE = 20; }
@expectedExceptionMessage
标注的运做方式相似于 @expectedExceptionCode
,用它能够对异常的错误讯息做出断言。
class MyTest extends PHPUnit_Framework_TestCase { /** * @expectedException MyException * @expectedExceptionMessage Some Message */ public function testExceptionHasRightMessage() { throw new MyException('Some Message', 20); } }
预期讯息能够是异常讯息的子串。在只须要断言传入的特定名称或参数确实出现于异常中时这个特性颇有用,这样就无需在测试中关注完整的异常讯息。
class MyTest extends PHPUnit_Framework_TestCase { /** * @expectedException MyException * @expectedExceptionMessage broken */ public function testExceptionHasRightMessage() { $param = "broken"; throw new MyException('Invalid parameter "'.$param.'".', 20); } }
为了方便测试同时减小冗余,能够用"@expectedExceptionMessage ClassName::CONST
"这样的语法将指定类常量做为 @expectedExceptionMessage
。在“@expectedExceptionCode”一节中能够看到范例。
预期讯息也能够经过 @expectedExceptionMessageRegExp
标注以正则表达式来指定。当没法用子串来完成对给定讯息的匹配时,这种方式就很是有用了。
class MyTest extends PHPUnit_Framework_TestCase { /** * @expectedException MyException * @expectedExceptionMessageRegExp /Argument \d+ can not be an? \w+/ */ public function testExceptionHasRightMessage() { throw new MyException('Argument 2 can not be an integer'); } }
测试能够用 @group
标注来标记为属于一个或多个组,就像这样:
class MyTest extends PHPUnit_Framework_TestCase { /** * @group specification */ public function testSomething() { } /** * @group regresssion * @group bug2204 */ public function testSomethingElse() { } }
测试能够基于组来选择性的执行,使用命令行测试执行器的 --group
and --exclude-group
选项,或者使用对应的 XML 配置文件指令。
若是安装了 PHP_Invoker
组件包并启用了严格模式,一个执行时间超过60秒的大型(large)测试将视为失败。这个超时限制能够经过 XML 配置文件的 timeoutForLargeTests
属性进行配置。
@medium
标注是 @group medium
的别名。中型(medium)测试不能依赖于标记为 @large
的测试。
若是安装了 PHP_Invoker
组件包并启用了严格模式,一个执行时间超过10秒的中型(medium)测试将视为失败。这个超时限制能够经过 XML 配置文件的 timeoutForMediumTests
属性进行配置。
在单独的进程中运行测试时,PHPUnit 会尝试保持来自父进程的全局状态(经过在父进程序列化全局状态而后在子进程反序列化的方式)。这当父进程包含非可序列化的全局内容时可能会致使问题。为了修正这种问题,能够用 @preserveGlobalState
标注来禁止 PHPUnit 保持全局状态。
class MyTest extends PHPUnit_Framework_TestCase { /** * @runInSeparateProcess * @preserveGlobalState disabled */ public function testInSeparateProcess() { // ... } }
指明单个测试类内的全部测试要各自运行在独立的 PHP 进程中。
/** * @runTestsInSeparateProcesses */ class MyTest extends PHPUnit_Framework_TestCase { // ... }
注意:“@preserveGlobalState”一节 默认状况下,PHPUnit 会尝试经过在父进程序列化全局状态而后在子进程反序列化的方式在子进程中保持来自父进程的全局状态。这当父进程包含非可序列化的全局内容时可能会致使问题。关于如何修正此问题的信息参见“@preserveGlobalState”一节。
class MyTest extends PHPUnit_Framework_TestCase { /** * @runInSeparateProcess */ public function testInSeparateProcess() { // ... } }
注意:“@preserveGlobalState”一节 默认状况下,PHPUnit 会尝试经过在父进程序列化全局状态而后在子进程反序列化的方式在子进程中保持来自父进程的全局状态。这当父进程包含非可序列化的全局内容时可能会致使问题。关于如何修正此问题的信息参见“@preserveGlobalState”一节。
@small
标注是 @group small
的别名。小型(small)测试不能依赖于标记为 @medium
或 @large
的测试。
若是安装了 PHP_Invoker
组件包并启用了严格模式,一个执行时间超过1秒的小型(small)测试将会视为失败。这个超时限制能够经过 XML 配置文件的 timeoutForSmallTests
属性进行配置。
默认状况下,全部未标记为 @medium
或 @large
的测试都视为小型(small)测试。请注意,虽然如此,--group
和有关的选项都只会将用恰当的标注显式标记好的测试视为在 small
组中。
除了用 test
做为测试方法名称的前缀外,还能够在方法的文档注释块中用 @test
标注来将其标记为测试方法。
/** * @test */ public function initialBalanceShouldBe0() { $this->assertEquals(0, $this->ba->getBalance()); }
@uses
标注用来指明那些将会在测试中执行到但同时又不打算让其被测试所覆盖的代码。在对代码单元进行测试时所必须的值对象就是个很好的例子。
/** * @covers BankAccount::deposit * @uses Money */ public function testMoneyCanBeDepositedInAccount() { // ... }
在严格覆盖模式中,意外覆盖的代码将致使测试断定为失败,这个标注就显得特别有用。关于严格覆盖模式的更多信息,参见“意外的代码覆盖”一节。
<phpunit>
元素的属性用于配置 PHPUnit 的核心功能。
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd" backupGlobals="true" backupStaticAttributes="false" <!--bootstrap="/path/to/bootstrap.php"--> cacheTokens="false" colors="false" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" forceCoversAnnotation="false" mapTestClassNameToCoveredClassName="false" printerClass="PHPUnit_TextUI_ResultPrinter" <!--printerFile="/path/to/ResultPrinter.php"--> processIsolation="false" stopOnError="false" stopOnFailure="false" stopOnIncomplete="false" stopOnSkipped="false" stopOnRisky="false" testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader" <!--testSuiteLoaderFile="/path/to/StandardTestSuiteLoader.php"--> timeoutForSmallTests="1" timeoutForMediumTests="10" timeoutForLargeTests="60" verbose="false"> <!-- ... --> </phpunit>
以上 XML 配置对应于在“命令行选项”一节描述过的 TextUI 测试执行器的默认行为。
其余那些不能用命令行选项来配置的选项有:
convertErrorsToExceptions
默认状况下,PHPUnit 将会安插一个错误处理函数来将如下错误转换为异常:
E_WARNING
E_NOTICE
E_USER_ERROR
E_USER_WARNING
E_USER_NOTICE
E_STRICT
E_RECOVERABLE_ERROR
E_DEPRECATED
E_USER_DEPRECATED
将 convertErrorsToExceptions
设为 false
能够禁用此功能。
convertNoticesToExceptions
此选项设置为 false
时,由 convertErrorsToExceptions
安插的错误处理函数不会将 E_NOTICE
、E_USER_NOTICE
、E_STRICT
错误转换为异常。
convertWarningsToExceptions
此选项设置为 false
时,由 convertErrorsToExceptions
安插的错误处理函数不会将 E_WARNING
或 E_USER_WARNING
错误转换为异常。
forceCoversAnnotation
只记录使用了 @covers
标注(文档参见“@covers”一节)的测试的代码覆盖率。
timeoutForLargeTests
若是实行了基于测试规模的时间限制,那么此属性为全部标记为 @large
的测试设定超时限制。在配置的时间限制内未执行完毕的测试将视为失败。
timeoutForMediumTests
若是实行了基于测试规模的时间限制,那么此属性为全部标记为 @medium
的测试设定超时限制。在配置的时间限制内未执行完毕的测试将视为失败。
timeoutForSmallTests
若是实行了基于测试规模的时间限制,那么此属性为全部未标记为 @medium
或 @large
的测试设定超时限制。在配置的时间限制内未执行完毕的测试将视为失败。
带有一个或多个 <testsuite>
子元素的 <testsuites>
元素用于将测试套件及测试用例组合出新的测试套件。
<testsuites> <testsuite name="My Test Suite"> <directory>/path/to/*Test.php files</directory> <file>/path/to/MyTest.php</file> <exclude>/path/to/exclude</exclude> </testsuite> </testsuites>
能够用 phpVersion
和 phpVersionOperator
属性来指定 PHP 版本需求。在如下例子中,仅当 PHP 版本至少为 5.3.0 时才会将 /path/to/*Test.php
文件与 /path/to/MyTest.php
文件添加到测试套件中。
<testsuites> <testsuite name="My Test Suite"> <directory suffix="Test.php" phpVersion="5.3.0" phpVersionOperator=">=">/path/to/files</directory> <file phpVersion="5.3.0" phpVersionOperator=">=">/path/to/MyTest.php</file> </testsuite> </testsuites>
phpVersionOperator
属性是可选的,其默认值为 >=
。
<groups>
元素及其 <include>
、<exclude>
、<group>
子元素用于从带有 @group
标注(相关文档参见 “@group”一节)的测试中选择须要运行(或不运行)的分组。
<groups> <include> <group>name</group> </include> <exclude> <group>name</group> </exclude> </groups>
以上 XML 配置对应于以以下选项调用 TextUI 测试执行器:
--group name
--exclude-group name
<filter>
元素及其子元素用于配置代码覆盖率报告所使用的白名单。
<filter> <whitelist processUncoveredFilesFromWhitelist="true"> <directory suffix=".php">/path/to/files</directory> <file>/path/to/file</file> <exclude> <directory suffix=".php">/path/to/files</directory> <file>/path/to/file</file> </exclude> </whitelist> </filter>
<logging>
元素及其 <log>
子元素用于配置测试执行期间的日志记录。
<logging> <log type="coverage-html" target="/tmp/report" lowUpperBound="35" highLowerBound="70"/> <log type="coverage-clover" target="/tmp/coverage.xml"/> <log type="coverage-php" target="/tmp/coverage.serialized"/> <log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/> <log type="json" target="/tmp/logfile.json"/> <log type="tap" target="/tmp/logfile.tap"/> <log type="junit" target="/tmp/logfile.xml" logIncompleteSkipped="false"/> <log type="testdox-html" target="/tmp/testdox.html"/> <log type="testdox-text" target="/tmp/testdox.txt"/> </logging>
以上 XML 配置对应于以以下选项调用 TextUI 测试执行器:
--coverage-html /tmp/report
--coverage-clover /tmp/coverage.xml
--coverage-php /tmp/coverage.serialized
--coverage-text
--log-json /tmp/logfile.json
> /tmp/logfile.txt
--log-tap /tmp/logfile.tap
--log-junit /tmp/logfile.xml
--testdox-html /tmp/testdox.html
--testdox-text /tmp/testdox.txt
lowUpperBound
、highLowerBound
、logIncompleteSkipped
及 showUncoveredFiles
属性没有等价的 TextUI 测试执行器选项。
lowUpperBound
:视为“低”覆盖率的最大覆盖率百分比。
highLowerBound
:视为“高”覆盖率的最小覆盖率百分比。
showUncoveredFiles
:在 --coverage-text
输出中显示全部符合白名单的文件,不只限于有覆盖率信息的那些。
showOnlySummary
:在 --coverage-text
输出中只显示摘要。
<listeners>
元素及其 <listener>
子元素用于在测试执行期间附加额外的测试监听器。
<listeners> <listener class="MyListener" file="/optional/path/to/MyListener.php"> <arguments> <array> <element key="0"> <string>Sebastian</string> </element> </array> <integer>22</integer> <string>April</string> <double>19.78</double> <null/> <object class="stdClass"/> </arguments> </listener> </listeners>
以上 XML 配置对应于将 $listener
对象(见下文)附到测试执行过程上。
$listener = new MyListener( array('Sebastian'), 22, 'April', 19.78, NULL, new stdClass );
<php>
元素及其子元素用于配置 PHP 设置、常量以及全局变量。同时也可用于向 include_path
前部置入内容。
<php> <includePath>.</includePath> <ini name="foo" value="bar"/> <const name="foo" value="bar"/> <var name="foo" value="bar"/> <env name="foo" value="bar"/> <post name="foo" value="bar"/> <get name="foo" value="bar"/> <cookie name="foo" value="bar"/> <server name="foo" value="bar"/> <files name="foo" value="bar"/> <request name="foo" value="bar"/> </php>
以上 XML 配置对应于以下 PHP 代码:
ini_set('foo', 'bar'); define('foo', 'bar'); $GLOBALS['foo'] = 'bar'; $_ENV['foo'] = 'bar'; $_POST['foo'] = 'bar'; $_GET['foo'] = 'bar'; $_COOKIE['foo'] = 'bar'; $_SERVER['foo'] = 'bar'; $_FILES['foo'] = 'bar'; $_REQUEST['foo'] = 'bar';
<selenium>
元素及其 <browser>
子元素用于配置 Selenium RC 服务器列表。
<selenium> <browser name="Firefox on Linux" browser="*firefox /usr/lib/firefox/firefox-bin" host="my.linux.box" port="4444" timeout="30000"/> </selenium>
以上 XML 配置对应于以下 PHP 代码:
class WebTest extends PHPUnit_Extensions_SeleniumTestCase { public static $browsers = array( array( 'name' => 'Firefox on Linux', 'browser' => '*firefox /usr/lib/firefox/firefox-bin', 'host' => 'my.linux.box', 'port' => 4444, 'timeout' => 30000 ) ); // ... }
Copyright (c) 2005-2015 Sebastian Bergmann. 此做品依照 Creative Commons Attribution 3.0 Unported License 受权。 如下是此受权许可协议的摘要信息,完整的法律文件附在其后。 -------------------------------------------------------------------- 您能够自由地: * 分享 - 复制、分发、传播此做品 * 重组 - 创做演绎此做品 唯须遵照下列条件: 姓名标示。弄必须按照做者或者版权人指定的方式表彰其姓名(但不得以任何方式暗示他们承认你或你使用本做品的方式)。 * 在再使用或者发行本做品时,您必须向他人明示本做品使用的许可协议条款。明示的最佳方法是附上本网页的连接。 * 若您得到著做权人准许,则上述全部条件均可予以避免除。 * 此协议对做者的人身权不构成任何损害与限制。 合理使用及其余权利不受许可协议影响。 以上是易于常人了解的法律条文(完整的受权许可协议)摘要。 ==================================================================== Creative Commons Legal Code Attribution 3.0 Unported CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. License THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. 1. Definitions a. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. b. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined above) for the purposes of this License. c. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership. d. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. e. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. f. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three- dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. g. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. h. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. i. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. 2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. 3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: a. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; b. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified."; c. to Distribute and Publicly Perform the Work including as incorporated in Collections; and, d. to Distribute and Publicly Perform Adaptations. e. For the avoidance of doubt: i. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; ii. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, iii. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved. 4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: a. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(b), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(b), as requested. b. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv), consistent with Section 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4 (b) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. c. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise. 5. Representations, Warranties and Disclaimer UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. Termination a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. 8. Miscellaneous a. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. c. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. d. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. e. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. f. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of this License. Creative Commons may be contacted at http://creativecommons.org/. ====================================================================
https://phpunit.de/manual/current/zh_cn/phpunit-book.html