phpunit 的官方文档对如何使用 phpunit 进行了详细的说明。本人在通读文档后进行了一些概要提升, 同时摘录了一些示例 phpunit-demo ,便于以后 理解和查阅。
文档较为简洁,但是也涵盖了平时使用的基本用法,适合入门使用。
安装 phpunit 项目安装 1 composer require --dev phpunit/phpunit
使用 ./vendor/bin/phpunit
全局安装 1 composer global require --dev phpunit/phpunit
使用 phpunit
快速入门 基本格式
测试类命名: 类名 + Test
, eg FooClassTest
测试方法命名: test + 方法名
, eg testFoo
也可以使用注释 @test
来标注需要测试的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 use PHPUnit \Framework \TestCase ;class SampleTest extends TestCase { public function testSomething () { $this ->assertTrue(true , 'This should already work.' ); } public function something () { $this ->assertTrue(true , 'This should already work.' ); } }
测试依赖(@depends) 有一些测试方法需要依赖于另一个测试方法的返回值,此时需要使用测试依赖。测试依赖 通过注释 @depends
来标记。
下列中, depends 方法的 return 值作为 testConsumer 的参数传入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 use PHPUnit \Framework \TestCase ;class MultipleDependenciesTest extends TestCase { public function testProducerFirst () { $this ->assertTrue(true ); return 'first' ; } public function testProducerSecond () { $this ->assertTrue(true ); return 'second' ; } public function testConsumer ($a, $b) { $this ->assertSame('first' , $a); $this ->assertSame('second' , $b); } }
数据提供器(@dataProvider) 在依赖中,所依赖函数的返回值作为参数传入测试函数。除此之外,我们也可以用数据提供器 来定义传入的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 use PHPUnit \Framework \TestCase ;class DataTest extends TestCase { public function testAdd ($a, $b, $expected) { $this ->assertSame($expected, $a + $b); } public function additionProvider () { return [ 'adding zeros' => [0 , 0 , 0 ], 'zero plus one' => [0 , 1 , 1 ], 'one plus zero' => [1 , 0 , 1 ], 'one plus one' => [1 , 1 , 2 ], ]; } }
测试异常(expectException) 需要在测试方法的开始处声明断言,然后执行语句。而不是调用后再声明
也可以通过注释来声明 @expectedException
, @expectedExceptionCode
,@expectedExceptionMessage
, @expectedExceptionMessageRegExp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 use PHPUnit \Framework \TestCase ;class ExceptionTest extends TestCase { public function testException () { $this ->expectException(\Exception ::class); throw new \Exception ('test' ); } public function exceptionExpect () { throw new \Exception ('test' ); } }
测试 PHP 错误 通过提前添加期望,来使测试正常进行,而不会报出 PHP 错误
1 2 3 4 5 6 7 8 9 10 11 12 use PHPUnit \Framework \TestCase ;class ExpectedErrorTest extends TestCase { public function testFailingInclude () { include 'not_existing_file.php' ; } }
测试输出 直接添加期望输出,然后执行相关函数。和测试异常类似,需要先添加期望,再执行代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 use PHPUnit \Framework \TestCase ;class OutputTest extends TestCase { public function testExpectFooActualFoo () { $this ->expectOutputString('foo' ); print 'foo' ; } public function testExpectBarActualBaz () { $this ->expectOutputString('bar' ); print 'baz' ; } }
命令行测试 此章节主要说明了命令行的一些格式和可用参数。可以参考官方文档 获取细节。The Command-Line Test Runner
基境 基境就是在测试前需要准备的一系列东西。
比如有的测试需要依赖数据库的数据,那么在测试类运作前需要进行数据的准备。
主要有两个函数 setUp
和 tearDown
。
那为什么不直接用构造函数和析构函数呢?是因为这两个有他用,当然你可可以直接用构造函数,然后 再执行 parent::__construct
,但不是麻烦嘛;
组织你的测试代码 可以通过命令行的 --bootstrap
参数来指定启动文件,用于文件加载。正常情况下,可以指向 composer 的 autoload 文件。 也可以在配置文件中配置(推荐)。
1 2 3 4 5 6 7 8 $ phpunit --bootstrap src/autoload.php tests PHPUnit |version|.0 by Sebastian Bergmann and contributors. ................................. Time: 636 ms, Memory: 3.50Mb OK (33 tests, 52 assertions)
1 2 3 4 5 6 7 <phpunit bootstrap ="src/autoload.php" > <testsuites > <testsuite name ="money" > <directory > tests</directory > </testsuite > </testsuites > </phpunit >
有风险的测试(Risky Tests) 无用测试(Useless Tests) 默认情况下,如果你的测试函数没有添加预期或者断言,就会被认为是无用测试。
通过设置 --dont-report-useless-tests
命令行参数,或者在 xml 配置 文件中配置 beStrictAboutTestsThatDoNotTestAnything="false"
来更改 这一默认行为。
意外的代码覆盖(Unintentionally Covered Code) 当打开这个配置后,如果使用 @covers
注释来包含一些文件的覆盖报告,就会被 判定为有风险的测试。
通过设置 --strict-coverage
命令行参数,或者在 xml 配置 文件中配置 beStrictAboutCoversAnnotation="true"
来更改 这一默认行为。
测试过程中有输出(Output During Test Execution) 如果在测试过程中输出文本,则会被认定为有风险的测试。
通过设置 --disallow-test-output
命令行参数,或者在 xml 配置 文件中配置 beStrictAboutOutputDuringTests="true"
来更改 这一默认行为。
测试超时(Test Execution Timeout) 通过注释来限制某些测试不能超过一定时间:
@large 60秒
@medium 10秒
@small 1秒
通过设置 --enforce-time-limit
命令行参数,或者在 xml 配置 文件中配置 enforceTimeLimit="true"
来更改 这一默认行为。
操作全局状态(Global State Manipulation) phpunit 可以对全局状态进行检测。
通过设置 --strict-global-state
命令行参数,或者在 xml 配置 文件中配置 beStrictAboutChangesToGlobalState="true"
来更改 这一默认行为。
待完善的测试和跳过的测试 处于一些原因,我们希望跳过或者对某些测试方法标记未待完善
待完善的测试 使用 $this->markTestIncomplete
标记待完善的测试
1 2 3 4 5 6 7 8 9 10 11 12 use PHPUnit \Framework \TestCase ;class SampleTest extends TestCase { public function testSomething () { $this ->assertTrue(true , 'This should already work.' ); $this ->markTestIncomplete( 'This test has not been implemented yet.' ); }
跳过的测试 使用 markTestSkipped
来标记跳过的测试。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use PHPUnit \Framework \TestCase ;class DatabaseTest extends TestCase { protected function setUp () { if (!extension_loaded('mysqli' )) { $this ->markTestSkipped( 'The MySQLi extension is not available.' ); } } public function testConnection () { } }
结合 @require 来跳过测试 以下的示例中,使用 require 来限定测试需要依赖 mysqli 拓展和 5.3 以上 的 PHP 版本,否则跳过测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 use PHPUnit \Framework \TestCase ;class DatabaseTest extends TestCase { public function testConnection () { } }
数据库测试 使用数据库测试之前先安装拓展 composer require --dev phpunit/dbunit
。
总的来说,我们在好多的测试场景中都会用到数据库,我们可以结合 PHPUnit 的基境 章节中提到的 setUp
来进行测试。
我们来看一个示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 use PHPUnit \DbUnit \DataSet \ArrayDataSet ;use PHPUnit \DbUnit \TestCaseTrait ;use PHPUnit \Framework \TestCase ;class ConnectionTest extends TestCase { use TestCaseTrait ; public function getConnection () { $pdo = new \PDO( 'mysql:host=127.0.0.1:33060;dbname=phpunit;charset=utf8mb4' , 'root' , '112233' ); return $this ->createDefaultDBConnection($pdo); } public function getDataSet () { return new ArrayDataSet( [ 'guestbook' => [ [ 'id' => 1 , 'content' => 'Hello buddy!' , 'user' => 'joe' , 'created' => '2010-04-24 17:15:23' , ], [ 'id' => 2 , 'content' => 'I like it!' , 'user' => 'mike' , 'created' => '2010-04-26 12:14:20' , ], ], ] ); } public function testCreateDataSet () { $this ->markTestSkipped('just an example, skipped' ); $tableNames = ['guestbook' ]; $dataSet = $this ->getConnection()->createDataSet(); } public function testCreateQueryTable () { $this ->markTestSkipped('just an example, skipped' ); $tableNames = ['guestbook' ]; $queryTable = $this ->getConnection()->createQueryTable('guestbook' , 'SELECT * FROM guestbook' ); } public function testGetRowCount () { $this ->assertSame(2 , $this ->getConnection()->getRowCount('guestbook' )); } public function testAddEntry () { $queryTable = $this ->getConnection()->createQueryTable( 'guestbook' , 'SELECT * FROM guestbook' ); $expectedTable = $this ->createFlatXmlDataSet(__DIR__ .'/expectedBook.xml' ) ->getTable('guestbook' ); $this ->assertTablesEqual($expectedTable, $queryTable); } }
其中 getConnection
和 getDataSet
都是 TestCaseTrait
中提供的方法, 我们在 getConnection
中设定数据库的链接动作,同时在 getDataSet
中设定 需要往数据库中写入的数据,注意,每次执行这个测试类时,都会执行
清空数据库 TRUNCATE
填充数据
如何验证呢?加一个字段为 datetime 类型,设置位数据库自动更新时间,即可看到每次执行测试时,时间都在变化
测试桩 所谓的桩测试,其实就是对一些类的方法进行一番 mock,强制其返回一些数据。因为在开发中,有一些类 依赖于第三方服务,而第三方服务又属于“不可控”因素,所以这个时候就需要使用“桩”了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 use PHPUnit \Framework \TestCase ;class StubTest extends TestCase { public function testStub () { $stub = $this ->createMock(SomeClass::class); $stub->method('doSomething' ) ->willReturn('foo' ); $this ->assertSame('foo' , $stub->doSomething()); } public function testStub2 () { $stub = $this ->getMockBuilder(SomeClass::class) ->disableOriginalConstructor() ->disableOriginalClone() ->disableArgumentCloning() ->disallowMockingUnknownTypes() ->getMock(); $stub->method('doSomething' ) ->willReturn('foo' ); $this ->assertSame('foo' , $stub->doSomething()); } public function testReturnArgumentStub () { $stub = $this ->createMock(SomeClass::class); $stub->method('doSomething' ) ->will($this ->returnArgument(0 )); $this ->assertSame('foo' , $stub->doSomething('foo' )); $this ->assertSame('bar' , $stub->doSomething('bar' )); } public function testReturnSelf () { $stub = $this ->createMock(SomeClass::class); $stub->method('doSomething' ) ->will($this ->returnSelf()); $this ->assertSame($stub, $stub->doSomething()); } public function testReturnValueMapStub () { $stub = $this ->createMock(SomeClass::class); $map = [ ['a' , 'b' , 'c' , 'd' ], ['e' , 'f' , 'g' , 'h' ], ]; $stub->method('doSomething' ) ->will($this ->returnValueMap($map)); $this ->assertSame('d' , $stub->doSomething('a' , 'b' , 'c' )); $this ->assertSame('h' , $stub->doSomething('e' , 'f' , 'g' )); } public function testReturnCallbackStub () { $stub = $this ->createMock(SomeClass::class); $stub->method('doSomething' ) ->will($this ->returnCallback('str_rot13' )); $this ->assertSame('fbzrguvat' , $stub->doSomething('something' )); } public function testOnConsecutiveCallsStub () { $stub = $this ->createMock(SomeClass::class); $stub->method('doSomething' ) ->will($this ->onConsecutiveCalls(2 , 3 , 5 , 7 )); $this ->assertSame(2 , $stub->doSomething()); $this ->assertSame(3 , $stub->doSomething()); $this ->assertSame(5 , $stub->doSomething()); } public function testThrowExceptionStub () { $this ->expectException(\Exception ::class); $stub = $this ->createMock(SomeClass::class); $stub->method('doSomething' ) ->will($this ->throwException(new \Exception ())); $stub->doSomething(); } } class SomeClass { public function doSomething () { } }
我们看到, SomeClass
的 doSomething()
本身没有返回数据,我们通过桩动作,来完成了测试。
这个在测试依赖于第三方服务的相关代码时很管用。
代码覆盖度 白名单文件(Whitelisting Files) 添加到白名单文件的文件或者文件夹,会进行代码覆盖度统计的工作。具体的参数可以看帮助文档
### 忽略代码块(Ignoring Code Blocks)
我们可以添加部分代码不进行覆盖度统计。通过一些注释来标记即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 use PHPUnit \Framework \TestCase ;class Foo { public function bar () { } } class Bar { public function foo () { } } if (false ) { print '*' ; } exit ;
执行方法进行统计(Specifying Covered Methods) 同样通过添加注释标记的方法来执行需要覆盖的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 use PHPUnit \Framework \TestCase ;class BankAccountTest extends TestCase { protected $ba; protected function setUp () { $this ->ba = new BankAccount; } public function testBalanceIsInitiallyZero () { $this ->assertSame(0 , $this ->ba->getBalance()); } public function testBalanceCannotBecomeNegative () { try { $this ->ba->withdrawMoney(1 ); } catch (BankAccountException $e) { $this ->assertSame(0 , $this ->ba->getBalance()); return ; } $this ->fail(); } public function testBalanceCannotBecomeNegative2 () { try { $this ->ba->depositMoney(-1 ); } catch (BankAccountException $e) { $this ->assertSame(0 , $this ->ba->getBalance()); return ; } $this ->fail(); } public function testDepositWithdrawMoney () { $this ->assertSame(0 , $this ->ba->getBalance()); $this ->ba->depositMoney(1 ); $this ->assertSame(1 , $this ->ba->getBalance()); $this ->ba->withdrawMoney(1 ); $this ->assertSame(0 , $this ->ba->getBalance()); } }