PHPUnit 手册
PHPUnit 手册
Sebastian Bergmann
版权 © 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015 Sebastian Bergmann
本作品依据 Creative Commons Attribution 3.0 Unported 许可协议进行授权。
此版本对应于 PHPUnit 5.2。最后更新于 2016-03-15。
校验 PHPUnit PHAR 发行包用文件系统来编排测试套件用 XML 配置来编排测试套件7. 未完成的测试与跳过的测试数据库测试所支持的供应商PHPUnit 数据库测试用例的配置数据库构架(DDL)怎么办?小建议:使用你自己的抽象数据库 TestCase 类理解 DataSet(数据集)和 DataTable(数据表)对表中数据行的数量作出断言对多个表的状态作出断言PHPUnit 会为每个测试(重新)创建数据库吗?为了让数据库扩展模块正常工作,需要在应用程序中使用 PDO 吗? 如果看到“ Too much Connections”错误该怎么办? Flat XML / CSV 数据集中如何处理 NULL?仿件对象(Mock Object)对特质(Trait)与抽象类进行模仿对 Web 服务(Web Services)进行上桩或模仿13. Logging (日志记录)从 PHPUnit_Framework_TestCase 派生子类实现 PHPUnit_Framework_TestListener从 PHPUnit_Extensions_TestDecorator 派生子类实现 PHPUnit_Framework_TestassertClassHasAttribute()assertClassHasStaticAttribute()assertContainsOnlyInstancesOf()assertEqualXMLStructure()assertGreaterThanOrEqual()assertJsonFileEqualsJsonFile()assertJsonStringEqualsJsonFile()assertJsonStringEqualsJsonString()assertLessThanOrEqual()assertObjectHasAttribute()assertStringMatchesFormat()assertStringMatchesFormatFile()assertStringEndsWith()assertStringEqualsFile()assertStringStartsWith()assertXmlFileEqualsXmlFile()assertXmlStringEqualsXmlFile()assertXmlStringEqualsXmlString()@backupStaticAttributes@expectedExceptionCode@expectedExceptionMessage@expectedExceptionMessageRegExp@runTestsInSeparateProcesses@runInSeparateProcessWhitelisting Files for Code Coverage设定 PHP INI 设置、常量、全局变量为 Selenium RC 配置浏览器
第 1 章 安装 PHPUnit
PHPUnit 5.2 requires PHP 5.6; using the latest version of PHP is highly recommended.
PHPUnit 需要使用 dom和 json扩展,它们通常是默认启用的。
PHPUnit 还需要 pcre、 reflection、 spl扩展。这些标准扩展默认启用,并且除非修改 PHP 的构建系统和 C 源代码,否则无法禁用它们。
代码覆盖率分析报告功能需要 Xdebug(2.2.1以上)与 tokenizer扩展。生成 XML 格式的报告需要有 xmlwriter扩展。
PHP 档案包 (PHAR)
要获取 PHPUnit,最简单的方法是下载 PHPUnit 的 PHP 档案包 (PHAR),它将 PHPUnit 所需要的所有必要组件(以及某些可选组件)捆绑在单个文件中:
要使用 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
整体上说,在 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 项目分发的所有官方代码发行包都由发行包管理器进行签名。在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 phpunit.phar.ascgpg: Signature made Sat 19 Jul 2014 01:28:02 PM CEST using RSA key ID 6372C20Agpg: Can't check signature: public key not found
在本地系统中没有发行包管理器的公钥( 6372C20A)。为了能进行校验,必须从某个密钥服务器上取得发行包管理器的公钥。其中一个服务器是 pgp.uni-mainz.de。所有密钥服务器是链接在一起的,因此连接到任一密钥服务器都可以。
gpg --keyserver pgp.uni-mainz.de --recv-keys 0x4AA394086372C20Agpg: requesting key 6372C20A from hkp server pgp.uni-mainz.degpg: key 6372C20A: public key "Sebastian Bergmann" importedgpg: Total number processed: 1gpg: imported: 1 (RSA: 1)
现在已经取得了条目名称为"Sebastian Bergmann
gpg phpunit.phar.ascgpg: Signature made Sat 19 Jul 2014 01:28:02 PM CEST using RSA key ID 6372C20Agpg: Good signature from "Sebastian Bergmann"gpg: aka "Sebastian Bergmann "gpg: aka "Sebastian Bergmann "gpg: aka "Sebastian Bergmann "gpg: aka "Sebastian Bergmann "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 C20A
此时,签名已经没问题了,但是这个公钥还不能信任。签名没问题意味着文件未被篡改。可是由于公钥加密系统的性质,还需要再校验密钥 6372C20A确实是由真正的 Sebastian Bergmann 创建的。
任何攻击者都能创建公钥并将其上传到公钥服务器。他们可以建立一个带恶意的发行包,并用这个假密钥进行签名。这样,如果尝试对这个损坏了的发行包进行签名校验,由于密钥是“真”密钥,校验将成功完成。因此,需要对这个密钥的真实性进行校验。如何对公钥的真实性进行校验已经超出了本文档的范畴。
有个比较谨慎的做法是创建一个脚本来管理 PHPUnit 的安装,在运行测试套件之前校验 GnuPG 签名。例如:
#!/usr/bin/env bashclean=1 # 是否在测试完成之后删除 phpunit.phar ?aftercmd="php phpunit.phar --bootstrap bootstrap.php src/tests"gpg --fingerprint D8406D0D82947747293778314AA394086372C20Aif [ $? -ne 0 ]; then echo -e "\033[33mDownloading PGP Public Key...\033[0m" gpg --recv-keys D8406D0D82947747293778314AA394086372C20A # Sebastian Bergmanngpg --fingerprint D8406D0D82947747293778314AA394086372C20A if [ $? -ne 0 ]; then echo -e "\033[31mCould not download PGP public key for verification\033[0m" exit fifiif [ "$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 fifi# 抓取最新的发行版和对应的签名if [ ! -f phpunit.phar ]; then wget https://phar.phpunit.de/phpunit.pharfiif [ ! -f phpunit.phar.asc ]; then wget https://phar.phpunit.de/phpunit.phar.ascfi# 在运行前先校验gpg --verify phpunit.phar.asc phpunit.pharif [ $? -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 fielse 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 1fi
Composer
如果用 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 章 编写 PHPUnit 测试
展示了如何用 PHPUnit 编写测试来对 PHP 数组操作进行测试。本例介绍了用 PHPUnit 编写测试的基本惯例与步骤:
-
针对类 Class的测试写在类 ClassTest中。
-
ClassTest(通常)继承自 PHPUnit_Framework_TestCase。
-
测试都是命名为 test*的公用方法。
也可以在方法的文档注释块(docblock)中使用 @test标注将其标记为测试方法。
-
在测试方法内,类似于 assertEquals()(参见)这样的断言方法用来对实际值与预期值的匹配做出断言。
例 2.1: 用 PHPUnit 测试数组操作
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)); }}?>
当你想把一些东西写到 print语句或者调试表达式中时,别这么做,将其写成一个测试来代替。 | ||
-- Martin Fowler |
测试的依赖关系
单元测试主要是作为一种良好实践来编写的,它能帮助开发人员识别并修复 bug、重构代码,还可以看作被测软件单元的文档。要实现这些好处,理想的单元测试应当覆盖程序中所有可能的路径。一个单元测试通常覆盖一个函数或方法中的一个特定路径。但是,测试方法并不一定非要是一个封装良好的独立实体。测试方法之间经常有隐含的依赖关系暗藏在测试的实现方案中。 | ||
-- Adrian Kuhn et. al. |
PHPUnit支持对测试方法之间的显式依赖关系进行声明。这种依赖关系并不是定义在测试方法的执行顺序中,而是允许生产者(producer)返回一个测试基境(fixture)的实例,并将此实例传递给依赖于它的消费者(consumer)们。
-
生产者(producer),是能生成被测单元并将其作为返回值的测试方法。
-
消费者(consumer),是依赖于一个或多个生产者及其返回值的测试方法。
展示了如何用 @depends标注来表达测试方法之间的依赖关系。
例 2.2: 用 @depends标注来表达依赖关系
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: 利用测试之间的依赖关系
assertTrue(FALSE); } /** * @depends testOne */ public function testTwo() { }}?>
phpunit --verbose DependencyFailureTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors.FSTime: 0 seconds, Memory: 5.00MbThere was 1 failure:1) DependencyFailureTest::testOneFailed asserting that false is true./home/sb/DependencyFailureTest.php:6There was 1 skipped test:1) DependencyFailureTest::testTwoThis test depends on "DependencyFailureTest::testOne" to pass.FAILURES!Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.
测试可以使用多个 @depends标注。PHPUnit 不会更改测试的运行顺序,因此你需要自行保证某个测试所依赖的所有测试均出现于这个测试之前。
拥有多个 @depends标注的测试,其第一个参数是第一个生产者提供的基境,第二个参数是第二个生产者提供的基境,以此类推。参见
例 2.4: 有多重依赖的测试
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 --verbose MultipleDependenciesTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors....Time: 0 seconds, Memory: 3.25MbOK (3 tests, 3 assertions)
数据供给器
测试方法可以接受任意参数。这些参数由数据供给器方法(在中,是 additionProvider()方法)提供。用 @dataProvider标注来指定使用哪个数据供给器方法。
数据供给器方法必须声明为 public,其返回值要么是一个数组,其每个元素也是数组;要么是一个实现了 Iterator接口的对象,在对它进行迭代时每步产生一个数组。每个数组都是测试数据集的一部分,将以它的内容作为参数来调用测试方法。
例 2.5: 使用返回数组的数组的数据供给器
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 DataTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors....FTime: 0 seconds, Memory: 5.75MbThere was 1 failure:1) DataTest::testAdd with data set #3 (1, 1, 3)Failed asserting that 2 matches expected 3./home/sb/DataTest.php:9FAILURES!Tests: 4, Assertions: 4, Failures: 1.
当使用到大量数据集时,最好逐个用字符串键名对其命名,避免用默认的数字键名。这样输出信息会更加详细些,其中将包含打断测试的数据集所对应的名称。
例 2.6: 使用带有命名数据集的数据供给器
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 DataTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors....FTime: 0 seconds, Memory: 5.75MbThere 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:9FAILURES!Tests: 4, Assertions: 4, Failures: 1.
例 2.7: 使用返回迭代器对象的数据供给器
assertEquals($expected, $a + $b); } public function additionProvider() { return new CsvFileIterator('data.csv'); }}?>
phpunit DataTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors....FTime: 0 seconds, Memory: 5.75MbThere was 1 failure:1) DataTest::testAdd with data set #3 ('1', '1', '3')Failed asserting that 2 matches expected '3'./home/sb/DataTest.php:11FAILURES!Tests: 4, Assertions: 4, Failures: 1.
例 2.8: CsvFileIterator 类
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: 在同一个测试中组合使用 @depends 和 @dataProvider
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 --verbose DependencyAndDataProviderComboTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors....FTime: 0 seconds, Memory: 3.50MbThere 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:31FAILURES!Tests: 4, Assertions: 4, Failures: 1.
注意
如果一个测试依赖于另外一个使用了数据供给器的测试,仅当被依赖的测试至少能在一组数据上成功时,依赖于它的测试才会运行。使用了数据供给器的测试,其运行结果是无法注入到依赖于此测试的其他测试中的。
注意
所有的数据供给器方法的执行都是在对 setUpBeforeClass静态方法的调用和第一次对 setUp方法的调用之前完成的。因此,无法在数据供给器中使用创建于这两个方法内的变量。这是必须的,这样 PHPUnit 才能计算测试的总数量。
对异常进行测试
展示了如何用 @expectException标注来测试被测代码中是否抛出了异常。
例 2.10: 使用 expectException() 方法
expectException(InvalidArgumentException::class); }}?>
phpunit ExceptionTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors.FTime: 0 seconds, Memory: 4.75MbThere was 1 failure:1) ExceptionTest::testExceptionExpected exception InvalidArgumentExceptionFAILURES!Tests: 1, Assertions: 1, Failures: 1.
除了 expectException()方法外,还有 expectExceptionCode()、 expectExceptionMessage()和 expectExceptionMessageRegExp()方法可以用于为被测代码所抛出的异常建立预期。
或者,也可以用 @expectedException、 @expectedExceptionCode、 @expectedExceptionMessage和 @expectedExceptionMessageRegExp标注来为被测代码所抛出的异常建立预期。展示了一个范例。
例 2.11: 使用 @expectedException 标注
phpunit ExceptionTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors.FTime: 0 seconds, Memory: 4.75MbThere was 1 failure:1) ExceptionTest::testExceptionExpected exception InvalidArgumentExceptionFAILURES!Tests: 1, Assertions: 1, Failures: 1.
对 PHP 错误进行测试
默认情况下,PHPUnit 将测试在执行中触发的 PHP 错误、警告、通知都转换为异常。利用这些异常,就可以,比如说,预期测试将触发 PHP 错误,如所示。
注意
PHP 的 error_reporting运行时配置会对 PHPUnit 将哪些错误转换为异常有所限制。如果在这个特性上碰到问题,请确认 PHP 的配置中没有抑制想要测试的错误类型。
例 2.12: 用 @expectedException 来预期 PHP 错误
phpunit -d error_reporting=2 ExpectedErrorTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors..Time: 0 seconds, Memory: 5.25MbOK (1 test, 1 assertion)
PHPUnit_Framework_Error_Notice和 PHPUnit_Framework_Error_Warning分别代表 PHP 通知与 PHP 警告。
注意
对异常进行测试是越明确越好的。对太笼统的类进行测试有可能导致不良副作用。因此,不再允许用 @expectedException或 setExpectedException()对 Exception类进行测试。
如果测试依靠会触发错误的 PHP 函数,例如 fopen,有时候在测试中使用错误抑制符会很有用。通过抑制住错误通知,就能对返回值进行检查,否则错误通知将会导致抛出 PHPUnit_Framework_Error_Notice。
例 2.13: 对会引发PHP 错误的代码的返回值进行测试
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 ErrorSuppressionTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors..Time: 1 seconds, Memory: 5.25MbOK (1 test, 1 assertion)
如果不使用错误抑制符,此测试将会失败,并报告 fopen(/is-not-writeable/file): failed to open stream: No such file or directory。
对输出进行测试
有时候,想要断言(比如说)某方法的运行过程中生成了预期的输出(例如,通过 echo或 print)。 PHPUnit_Framework_TestCase类使用 PHP 的 输出缓冲特性来为此提供必要的功能支持。
展示了如何用 expectOutputString()方法来设定所预期的输出。如果没有产生预期的输出,测试将计为失败。
例 2.14: 对函数或方法的输出进行测试
expectOutputString('foo'); print 'foo'; } public function testExpectBarActualBaz() { $this->expectOutputString('bar'); print 'baz'; }}?>
phpunit OutputTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors..FTime: 0 seconds, Memory: 5.75MbThere was 1 failure:1) OutputTest::testExpectBarActualBazFailed asserting that two strings are equal.--- Expected+++ Actual@@ @@-'bar'+'baz'FAILURES!Tests: 2, Assertions: 2, Failures: 1.
中列举了用于对输出进行测试的各种方法。
表 2.1. 用于对输出进行测试的方法
方法 | 含义 |
---|---|
void expectOutputRegex(string $regularExpression) | 设置输出预期为输出应当匹配正则表达式 $regularExpression。 |
void expectOutputString(string $expectedString) | 设置输出预期为输出应当与 $expectedString字符串相等。 |
bool setOutputCallback(callable $callback) | 设置回调函数,用来做诸如将实际输出规范化之类的动作。 |
注意
在严格模式下,本身产生输出的测试将会失败。
错误相关信息的输出
当有测试失败时,PHPUnit 全力提供尽可能多的有助于找出问题所在的上下文信息。
例 2.15: 数组比较失败时生成的错误相关信息输出
assertEquals( array(1,2,3 ,4,5,6), array(1,2,33,4,5,6) ); }}?>
phpunit ArrayDiffTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors.FTime: 0 seconds, Memory: 5.25MbThere was 1 failure:1) ArrayDiffTest::testEqualityFailed 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:7FAILURES!Tests: 1, Assertions: 1, Failures: 1.
在这个例子中,数组中只有一个值不同,但其他值也都同时显示出来,以提供关于错误发生的位置的上下文信息。
当生成的输出很长而难以阅读时,PHPUnit 将对其进行分割,并在每个差异附近提供少数几行上下文信息。
例 2.16: 长数组比较失败时生成的错误相关信息输出
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 LongArrayDiffTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors.FTime: 0 seconds, Memory: 5.25MbThere was 1 failure:1) LongArrayDiffTest::testEqualityFailed asserting that two arrays are equal.--- Expected+++ Actual@@ @@ 13 => 2- 14 => 3+ 14 => 33 15 => 4 16 => 5 17 => 6 )/home/sb/LongArrayDiffTest.php:7FAILURES!Tests: 1, Assertions: 1, Failures: 1.
边缘情况
当比较失败时,PHPUnit 为输入值建立文本表示,然后以此进行对比。这种实现导致在差异指示中显示出来的问题可能比实际上存在的多。
这种情况只出现在对数组或者对象使用 assertEquals 或其他“弱”比较函数时。
例 2.17: 当使用弱比较时在生成的差异结果中出现的边缘情况
assertEquals( array(1 ,2,3 ,4,5,6), array('1',2,33,4,5,6) ); }}?>
phpunit ArrayWeakComparisonTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors.FTime: 0 seconds, Memory: 5.25MbThere was 1 failure:1) ArrayWeakComparisonTest::testEqualityFailed 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:7FAILURES!Tests: 1, Assertions: 1, Failures: 1.
在这个例子中,第一个索引项中的 1and '1'在报告中被视为不同,虽然 assertEquals 认为这两个值是匹配的。
第 3 章 命令行测试执行器
PHPUnit 命令行测试执行器可通过 phpunit命令调用。下面的代码展示了如何用 PHPUnit 命令行测试执行器来运行测试:
phpunit ArrayTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors...Time: 0 secondsOK (2 tests, 2 assertions)
上面这个调用例子中,PHPUnit 命令行测试执行器将在当前工作目录中寻找 ArrayTest.php源文件并加载之。而在此源文件中应当能找到 ArrayTest测试用例类,此类中的测试将被执行。
对于每个测试的运行,PHPUnit 命令行工具输出一个字符来指示进展:
.
当测试成功时输出。
F
当测试方法运行过程中一个断言失败时输出。
E
当测试方法运行过程中产生一个错误时输出。
R
当测试被标记为有风险时输出(参见)。
S
当测试被跳过时输出(参见)。
I
当测试被标记为不完整或未实现时输出(参见)。
PHPUnit 区分 败(failure)与 错误(error)。失败指的是被违背了的 PHPUnit 断言,例如一个失败的 assertEquals()调用。错误指的是意料之外的异常(exception)或 PHP 错误。这种差异已被证明在某些时候是非常有用的,因为错误往往比失败更容易修复。如果得到了一个非常长的问题列表,那么最好先对付错误,当错误全部修复了之后再试一次瞧瞧还有没有失败。
命令行选项
让我们来瞧瞧以下代码中命令行测试运行器的各种选项:
phpunit --helpPHPUnit 5.2.0 by Sebastian Bergmann and contributors.Usage: phpunit [options] UnitTest [UnitTest.php] phpunit [options]phpunit UnitTestCode Coverage Options: --coverage-clover Generate code coverage report in Clover XML format. --coverage-crap4j Generate code coverage report in Crap4J XML format. --coverage-html Generate code coverage report in HTML format. --coverage-php Export PHP_CodeCoverage object to file. --coverage-text= Generate code coverage report in text format. Default: Standard output. --coverage-xml Generate code coverage report in PHPUnit XML format.Logging Options: --log-junit Log test execution in JUnit XML format to file. --log-tap Log test execution in TAP format to file. --log-json Log test execution in JSON format. --testdox-html Write agile documentation in HTML format to file. --testdox-text Write agile documentation in Text format to file.Test Selection Options: --filter Filter which tests to run. --testsuite 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,.phptTest 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= Use colors in output ("never", "auto" or "always"). --columns 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 TestSuiteLoader implementation to use. --repeat Runs the test(s) repeatedly. --tap Report test execution progress in TAP format. --testdox Report test execution progress in TestDox format. --printer TestListener implementation to use.Configuration Options: --bootstrap A "bootstrap" PHP file that is run before the tests. -c|--configuration Read configuration from XML file. --no-configuration Ignore default configuration file (phpunit.xml). --include-path 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.
运行由 UnitTest类提供的测试。这个类应当在 UnitTest.php源文件中声明。
UnitTest这个类必须满足以下二个条件之一:要么它继承自 PHPUnit_Framework_TestCase;要么它提供 public static suite()方法,这个方法返回一个 PHPUnit_Framework_Test对象,比如,一个 PHPUnit_Framework_TestSuite类的实例。
phpunit UnitTest UnitTest.php
运行由 UnitTest类提供的测试。这个类应当在指定的源文件中声明。
--coverage-clover
为运行的测试生成带有代码覆盖率信息的 XML 格式的日志文件。更多细节请参见。
请注意,此功能仅当安装了 tokenizer 和 Xdebug 这两个 PHP 扩展后才可用。
--coverage-crap4j
生成 Crap4j 格式的代码覆盖率报告。更多细节请参见。
请注意,此功能仅当安装了 tokenizer 和 Xdebug 这两个 PHP 扩展后才可用。
--coverage-html
生成 HTML 格式的代码覆盖率报告。更多细节请参见。
请注意,此功能仅当安装了 tokenizer 和 Xdebug 这两个 PHP 扩展后才可用。
--coverage-php
生成一个序列化后的 PHP_CodeCoverage 对象,此对象含有代码覆盖率信息。
请注意,此功能仅当安装了 tokenizer 和 Xdebug 这两个 PHP 扩展后才可用。
--coverage-text
为运行的测试以人们可读的格式生成带有代码覆盖率信息的日志文件或命令行输出。更多细节请参见。
请注意,此功能仅当安装了 tokenizer 和 Xdebug 这两个 PHP 扩展后才可用。
--log-junit
为运行的测试生成 JUnit XML 格式的日志文件。更多细节请参见。
--log-tap
为运行的测试生成 Test Anything Protocol (TAP)格式的日志文件。更多细节请参见。
--log-json
生成 JSON格式的日志文件。更多细节请参见。
--testdox-html和 --testdox-text
为运行的测试以 HTML 或纯文本格式生成敏捷文档。更多细节请参见。
--filter
只运行名称与给定模式匹配的测试。如果模式未闭合包裹于分隔符,PHPUnit 将用 /分隔符对其进行闭合包裹。
测试名称将以以下格式之一进行匹配:
TestNamespace\TestCaseClass::testMethod
默认的测试名称格式等价于在测试方法内使用 __METHOD__魔术常量。
TestNamespace\TestCaseClass::testMethod with data set #0
当测试拥有数据供给器时,数据的每轮迭代都会将其当前索引附加在默认测试名称结尾处。
TestNamespace\TestCaseClass::testMethod with data set "my named data"
当测试拥有使用命名数据集的数据供给器时,数据的每轮迭代都会将当前名称附加在默认测试名称结尾处。命名数据集的例子参见。
例 3.1: 命名数据集
assertTrue($data); } public function provider() { return array( 'my named data' => array(true), 'my data' => array(true) ); }}?>/path/to/my/test.phpt
对于 PHPT 测试,其测试名称是文件系统路径。
有效的过滤器模式例子参见。
例 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: 过滤器的快捷方式
-
--filter 'testMethod#2'
-
--filter 'testMethod#2-4'
-
--filter '#2'
-
--filter '#2-4'
-
--filter 'testMethod@my named data'
-
--filter 'testMethod@my.*data'
-
--filter '@my named data'
-
--filter '@my.*data'
只运行名称与给定模式匹配的测试套件。
--group
只运行来自指定分组(可以多个)的测试。可以用 @group标注为测试标记其所属的分组。
@author标注是 @group的一个别名,允许按作者来筛选测试。
--exclude-group
排除来自指定分组(可以多个)的测试。可以用 @group标注为测试标记其所属的分组。
--list-groups
列出所有有效的测试分组。
--test-suffix
只查找文件名以指定后缀(可以多个)结尾的测试文件。
--report-useless-tests
更严格对待事实上不测试任何内容的测试。详情参见。
--strict-coverage
更严格对待意外的代码覆盖。详情参见。
--strict-global-state
更严格对待全局状态篡改。详情参见。
--disallow-test-output
更严格对待测试执行期间产生的输出。详情参见。
--disallow-todo-tests
不执行文档注释块中含有 @todo标注的测试。
--enforce-time-limit
根据测试规模对其加上执行时长限制。详情参见。
--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)报告测试进度。更多细节请参见。
--testdox
将测试进度以敏捷文档方式报告。更多细节请参见。
--printer
指定要使用的结果输出器(printer)。输出器类必须扩展 PHPUnit_Util_Printer并且实现 PHPUnit_Framework_TestListener接口。
--bootstrap
在测试前先运行一个 "bootstrap" PHP 文件。
--configuration, -c
从 XML 文件中读取配置信息。更多细节请参见。
如果 phpunit.xml或 phpunit.xml.dist(按此顺序)存在于当前工作目录并且 未使用 --configuration,将自动从此文件中读取配置。
--no-configuration
忽略当前工作目录下的 phpunit.xml与 phpunit.xml.dist。
--include-path
向 PHP 的 include_path开头添加指定路径(可以多个)。
-d
设置指定的 PHP 配置选项的值。
注意
请注意,从 4.8 开始,选项不能放在参数之后。
第 4 章 基境(fixture)
在编写测试时,最费时的部分之一是编写代码来将整个场景设置成某个已知的状态,并在测试结束后将其复原到初始状态。这个已知的状态称为测试的 基境(fixture)。
在中,基境十分简单,就是存储在 $stack变量中的数组。然而,绝大多数时候基境均远比一个简单数组要复杂,用于建立基境的代码量也会随之增长。测试的真正内容就被淹没于建立基境带来的干扰中。当编写多个需要类似基境的测试时这个问题就变得更糟糕了。如果没有来自于测试框架的帮助,就不得不在写每一个测试时都将建立基境的代码重复一次。
PHPUnit 支持共享建立基境的代码。在运行某个测试方法前,会调用一个名叫 setUp()的模板方法。 setUp()是创建测试所用对象的地方。当测试方法运行结束后,不管是成功还是失败,都会调用另外一个名叫 tearDown()的模板方法。 tearDown()是清理测试所用对象的地方。
在中,我们在测试之间运用生产者-消费者关系来共享基境。这并非总是预期的方式,甚至有时是不可能的。展示了另外一个编写测试 StackTest的方式。在这个方式中,不再重用基境本身,而是重用建立基境的代码。首先声明一个实例变量, $stack,用来替代方法内的局部变量。然后把 array基境的建立放到 setUp()方法中。最后,从测试方法中去除冗余代码,在 assertEquals()断言方法中使用新引入的实例变量 $this->stack替代方法内的局部变量 $stack。
例 4.1: 用 setUp() 建立栈的基境
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: 展示所有可用模板方法的例子
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 TemplateMethodsTestPHPUnit 5.2.0 by Sebastian Bergmann and contributors.TemplateMethodsTest::setUpBeforeClassTemplateMethodsTest::setUpTemplateMethodsTest::assertPreConditionsTemplateMethodsTest::testOneTemplateMethodsTest::assertPostConditionsTemplateMethodsTest::tearDown.TemplateMethodsTest::setUpTemplateMethodsTest::assertPreConditionsTemplateMethodsTest::testTwoTemplateMethodsTest::tearDownTemplateMethodsTest::onNotSuccessfulTestFTemplateMethodsTest::tearDownAfterClassTime: 0 seconds, Memory: 5.25MbThere was 1 failure:1) TemplateMethodsTest::testTwoFailed asserting thatis true./home/sb/TemplateMethodsTest.php:30FAILURES!Tests: 2, Assertions: 2, Failures: 1.
setUp() 多 tearDown() 少
理论上说, setUp()和 tearDown()是精确对称的,但是实践中并非如此。实际上,只有在 setUp()中分配了诸如文件或套接字之类的外部资源时才需要实现 tearDown()。如果 setUp()中只创建纯 PHP 对象,通常可以略过 tearDown()。不过,如果在 setUp()中创建了大量对象,你可能想要在 tearDown()中 unset()指向这些对象的变量,这样它们就可以被垃圾回收机制回收掉。对测试用例对象的垃圾回收动作则是不可预知的。
如果两个基境建立工作略有不同的测试该怎么办?有两种可能:
-
如果两个 setUp()代码仅有微小差异,把有差异的代码内容从 setUp()移到测试方法内。
-
如果两个 setUp()是确实不一样,那么需要另外一个测试用例类。参考基境建立工作的不同之处来命名这个类。
基境共享
有几个好的理由来在测试之间共享基境,但是大部分情况下,在测试之间共享基境的需求都源于某个未解决的设计问题。
一个有实际意义的多测试间共享基境的例子是数据库连接:只登录数据库一次,然后重用此连接,而不是每个测试都建立一个新的数据库连接。这样能加快测试的运行。
用 setUpBeforeClass()和 tearDownAfterClass()模板方法来分别在测试用例类的第一个测试之前和最后一个测试之后连接与断开数据库。
例 4.3: 在同一个测试套件内的不同测试之间共享基境
需要反复强调的是:在测试之间共享基境会降低测试的价值。潜在的设计问题是对象之间并非松散耦合。如果解决掉潜在的设计问题并使用桩件(stub)(参见)来编写测试,就能达成更好的结果,而不是在测试之间产生运行时依赖并错过改进设计的机会。
全局状态
使用单件(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标注可以用来控制对全局变量的备份与还原操作。另外,还可以提供一个全局变量的黑名单,黑名单中的全局变量将被排除于备份与还原操作之外,就像这样:
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{ prot