- composer require --dev phpunit/phpunit
-
使用 ./vendor/bin/phpunit
- composer global require --dev phpunit/phpunit
-
使用 phpunit
也可以使用注释 @test 来标注需要测试的方法
- use PHPUnit\Framework\TestCase;
-
- class SampleTest extends TestCase
- {
- public function testSomething()
- {
- $this->assertTrue(true, 'This should already work.');
- }
-
- /**
- * @test
- */
- public function something()
- {
- $this->assertTrue(true, 'This should already work.');
- }
- }
-
有一些测试方法需要依赖于另一个测试方法的返回值,此时需要使用测试依赖。测试依赖 通过注释 @depends 来标记。
下列中, depends 方法的 return 值作为 testConsumer 的参数传入
- use PHPUnit\Framework\TestCase;
-
- class MultipleDependenciesTest extends TestCase
- {
- public function testProducerFirst()
- {
- $this->assertTrue(true);
- return 'first';
- }
-
- public function testProducerSecond()
- {
- $this->assertTrue(true);
- return 'second';
- }
-
- /**
- * @depends testProducerFirst
- * @depends testProducerSecond
- */
- public function testConsumer($a, $b)
- {
- $this->assertSame('first', $a);
- $this->assertSame('second', $b);
- }
- }
-
在依赖中,所依赖函数的返回值作为参数传入测试函数。除此之外,我们也可以用数据提供器 来定义传入的数据。
- use PHPUnit\Framework\TestCase;
-
- class DataTest extends TestCase
- {
- /**
- * @dataProvider additionProvider
- */
- public function testAdd($a, $b, $expected)
- {
- $this->assertSame($expected, $a + $b);
- }
-
- public function additionProvider()
- {
- return [
- 'adding zeros' => [0, 0, 0], // 0 + 0 = 0 pass
- 'zero plus one' => [0, 1, 1], // 0 + 1 = 1 pass
- 'one plus zero' => [1, 0, 1], // 1 + 0 = 1 pass
- 'one plus one' => [1, 1, 2], // 1 + 1 = 2 pass
- ];
- }
- }
-
需要在测试方法的开始处声明断言,然后执行语句。而不是调用后再声明
也可以通过注释来声明 @expectedException, @expectedExceptionCode, @expectedExceptionMessage, @expectedExceptionMessageRegExp
- use PHPUnit\Framework\TestCase;
-
- class ExceptionTest extends TestCase
- {
- public function testException()
- {
- $this->expectException(\Exception::class);
-
- throw new \Exception('test');
- }
-
- /**
- * @throws \Exception
- * @test
- * @expectedException \Exception
- */
- public function exceptionExpect()
- {
- throw new \Exception('test');
- }
- }
-
通过提前添加期望,来使测试正常进行,而不会报出 PHP 错误
- use PHPUnit\Framework\TestCase;
-
- class ExpectedErrorTest extends TestCase
- {
- /**
- * @expectedException PHPUnit\Framework\Error\Error
- */
- public function testFailingInclude()
- {
- include 'not_existing_file.php';
- }
- }
-
直接添加期望输出,然后执行相关函数。和测试异常类似,需要先添加期望,再执行代码。
- use PHPUnit\Framework\TestCase;
-
- class OutputTest extends TestCase
- {
- public function testExpectFooActualFoo()
- {
- $this->expectOutputString('foo');
- print 'foo';
- }
-
- public function testExpectBarActualBaz()
- {
- $this->expectOutputString('bar');
- print 'baz';
- }
- }
-
此章节主要说明了命令行的一些格式和可用参数。可以参考官方文档 获取细节。The Command-Line Test Runner(link:https://phpunit.readthedocs.io/en/7.4/textui.html)
基境就是在测试前需要准备的一系列东西。
比如有的测试需要依赖数据库的数据,那么在测试类运作前需要进行数据的准备。
主要有两个函数 setUp 和 tearDown。
那为什么不直接用构造函数和析构函数呢?是因为这两个有他用,当然你可可以直接用构造函数,然后再执行 parent::__construct,但不是麻烦嘛;
可以通过命令行的 --bootstrap 参数来指定启动文件,用于文件加载。正常情况下,可以指向 composer 的 autoload 文件。 也可以在配置文件中配置(推荐)。
- $ 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)
-
- <phpunit bootstrap="src/autoload.php">
- <testsuites>
- <testsuite name="money">
- <directory>tests</directory>
- </testsuite>
- </testsuites>
- </phpunit>
-
默认情况下,如果你的测试函数没有添加预期或者断言,就会被认为是无用测试。
通过设置 --dont-report-useless-tests 命令行参数,或者在 xml 配置文件中配置 beStrictAboutTestsThatDoNotTestAnything="false" 来更改这一默认行为。
当打开这个配置后,如果使用 @covers 注释来包含一些文件的覆盖报告,就会被 判定为有风险的测试。
通过设置 --strict-coverage 命令行参数,或者在 xml 配置文件中配置 beStrictAboutCoversAnnotation="true" 来更改这一默认行为。
如果在测试过程中输出文本,则会被认定为有风险的测试。
通过设置 --disallow-test-output 命令行参数,或者在 xml 配置文件中配置 beStrictAboutOutputDuringTests="true" 来更改这一默认行为。
通过注释来限制某些测试不能超过一定时间:
通过设置 --enforce-time-limit 命令行参数,或者在 xml 配置文件中配置 enforceTimeLimit="true" 来更改这一默认行为。
phpunit 可以对全局状态进行检测。
通过设置 --strict-global-state 命令行参数,或者在 xml 配置文件中配置 beStrictAboutChangesToGlobalState="true" 来更改这一默认行为。
处于一些原因,我们希望跳过或者对某些测试方法标记未待完善
使用 $this->markTestIncomplete 标记待完善的测试
- 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 来标记跳过的测试。
- 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 来限定测试需要依赖 mysqli 拓展和 5.3 以上 的 PHP 版本,否则跳过测试
- use PHPUnit\Framework\TestCase;
-
- /**
- * @requires extension mysqli
- */
- class DatabaseTest extends TestCase
- {
- /**
- * @requires PHP 5.3
- */
- public function testConnection()
- {
- // Test requires the mysqli extension and PHP >= 5.3
- }
-
- // ... All other tests require the mysqli extension
- }
-
使用数据库测试之前先安装拓展 composer require --dev phpunit/dbunit。
总的来说,我们在好多的测试场景中都会用到数据库,我们可以结合 PHPUnit 的基境 章节中提到的 setUp 来进行测试。
我们来看一个示例
- use PHPUnit\DbUnit\DataSet\ArrayDataSet;
- use PHPUnit\DbUnit\TestCaseTrait;
- use PHPUnit\Framework\TestCase;
-
- /**
- * 测试之前,需要在 MySQL 中新建数据库 phpunit,并且新建表 guestbook
- * CREATE TABLE `guestbook` (
- * `id` bigint(20) NOT NULL,
- * `content` varchar(255) COLLATE utf8mb4_bin NOT NULL,
- * `user` varchar(255) COLLATE utf8mb4_bin NOT NULL,
- * `created` datetime NOT NULL
- * ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
- * Class ConnectionTest.
- *
- * @requires extension pdo_mysql
- */
- class ConnectionTest extends TestCase
- {
- use TestCaseTrait;
-
- /**
- * @return \PHPUnit\DbUnit\Database\DefaultConnection
- */
- public function getConnection()
- {
- $pdo = new \PDO(
- 'mysql:host=127.0.0.1:33060;dbname=phpunit;charset=utf8mb4',
- 'root',
- '112233'
- );
-
- return $this->createDefaultDBConnection($pdo);
- }
-
- /**
- * 请注意,phpunit每次会先 TRUNCATE 数据库,然后把下面数组的数据插入进去.
- *
- * @return ArrayDataSet
- */
- 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 中设定 需要往数据库中写入的数据,注意,每次执行这个测试类时,都会执行
如何验证呢?加一个字段为 datetime 类型,设置位数据库自动更新时间,即可看到每次执行测试时,时间都在变化
所谓的桩测试,其实就是对一些类的方法进行一番 mock,强制其返回一些数据。因为在开发中,有一些类 依赖于第三方服务,而第三方服务又属于“不可控”因素,所以这个时候就需要使用“桩”了。
- use PHPUnit\Framework\TestCase;
-
- class StubTest extends TestCase
- {
- public function testStub()
- {
- // Create a stub for the SomeClass class.
- $stub = $this->createMock(SomeClass::class);
-
- // Configure the stub.
- $stub->method('doSomething')
- ->willReturn('foo');
-
- // Calling $stub->doSomething() will now return
- // 'foo'.
- $this->assertSame('foo', $stub->doSomething());
- }
-
- public function testStub2()
- {
- // Create a stub for the SomeClass class.
- $stub = $this->getMockBuilder(SomeClass::class)
- ->disableOriginalConstructor()
- ->disableOriginalClone()
- ->disableArgumentCloning()
- ->disallowMockingUnknownTypes()
- ->getMock();
-
- // Configure the stub.
- $stub->method('doSomething')
- ->willReturn('foo');
-
- // Calling $stub->doSomething() will now return
- // 'foo'.
- $this->assertSame('foo', $stub->doSomething());
- }
-
- public function testReturnArgumentStub()
- {
- // Create a stub for the SomeClass class.
- $stub = $this->createMock(SomeClass::class);
-
- // Configure the stub.
- $stub->method('doSomething')
- ->will($this->returnArgument(0));
-
- // $stub->doSomething('foo') returns 'foo'
- $this->assertSame('foo', $stub->doSomething('foo'));
-
- // $stub->doSomething('bar') returns 'bar'
- $this->assertSame('bar', $stub->doSomething('bar'));
- }
-
- public function testReturnSelf()
- {
- // Create a stub for the SomeClass class.
- $stub = $this->createMock(SomeClass::class);
-
- // Configure the stub.
- $stub->method('doSomething')
- ->will($this->returnSelf());
-
- // $stub->doSomething() returns $stub
- $this->assertSame($stub, $stub->doSomething());
- }
-
- public function testReturnValueMapStub()
- {
- // Create a stub for the SomeClass class.
- $stub = $this->createMock(SomeClass::class);
-
- // Create a map of arguments to return values.
- $map = [
- ['a', 'b', 'c', 'd'],
- ['e', 'f', 'g', 'h'],
- ];
-
- // Configure the stub.
- $stub->method('doSomething')
- ->will($this->returnValueMap($map));
-
- // $stub->doSomething() returns different values depending on
- // the provided arguments.
- $this->assertSame('d', $stub->doSomething('a', 'b', 'c'));
- $this->assertSame('h', $stub->doSomething('e', 'f', 'g'));
- }
-
- public function testReturnCallbackStub()
- {
- // Create a stub for the SomeClass class.
- $stub = $this->createMock(SomeClass::class);
-
- // Configure the stub.
- $stub->method('doSomething')
- ->will($this->returnCallback('str_rot13'));
-
- // $stub->doSomething($argument) returns str_rot13($argument)
- $this->assertSame('fbzrguvat', $stub->doSomething('something'));
- }
-
- /**
- * 按照指定顺序返回列表中的值
- */
- public function testOnConsecutiveCallsStub()
- {
- // 为 SomeClass 类创建桩件。
- $stub = $this->createMock(SomeClass::class);
-
- // 配置桩件。
- $stub->method('doSomething')
- ->will($this->onConsecutiveCalls(2, 3, 5, 7));
-
- // $stub->doSomething() 每次返回值都不同
- $this->assertSame(2, $stub->doSomething());
- $this->assertSame(3, $stub->doSomething());
- $this->assertSame(5, $stub->doSomething());
- }
-
- public function testThrowExceptionStub()
- {
- $this->expectException(\Exception::class);
-
- // 为 SomeClass 类创建桩件
- $stub = $this->createMock(SomeClass::class);
-
- // 配置桩件。
- $stub->method('doSomething')
- ->will($this->throwException(new \Exception()));
-
- // $stub->doSomething() 抛出异常
- $stub->doSomething();
- }
- }
-
- class SomeClass
- {
- public function doSomething()
- {
- // Do something.
- }
- }
-
我们看到, SomeClass 的 doSomething() 本身没有返回数据,我们通过桩动作,来完成了测试。
这个在测试依赖于第三方服务的相关代码时很管用。
添加到白名单文件的文件或者文件夹,会进行代码覆盖度统计的工作。具体的参数可以看帮助文档
我们可以添加部分代码不进行覆盖度统计。通过一些注释来标记即可
- use PHPUnit\Framework\TestCase;
-
- /**
- * @codeCoverageIgnore
- */
- class Foo
- {
- public function bar()
- {
- }
- }
-
- class Bar
- {
- /**
- * @codeCoverageIgnore
- */
- public function foo()
- {
- }
- }
-
- if (false) {
- // @codeCoverageIgnoreStart
- print '*';
- // @codeCoverageIgnoreEnd
- }
-
- exit; // @codeCoverageIgnore
-
同样通过添加注释标记的方法来执行需要覆盖的方法。
- use PHPUnit\Framework\TestCase;
-
- class BankAccountTest extends TestCase
- {
- protected $ba;
-
- protected function setUp()
- {
- $this->ba = new BankAccount;
- }
-
- /**
- * @covers BankAccount::getBalance
- */
- public function testBalanceIsInitiallyZero()
- {
- $this->assertSame(0, $this->ba->getBalance());
- }
-
- /**
- * @covers BankAccount::withdrawMoney
- */
- public function testBalanceCannotBecomeNegative()
- {
- try {
- $this->ba->withdrawMoney(1);
- }
-
- catch (BankAccountException $e) {
- $this->assertSame(0, $this->ba->getBalance());
-
- return;
- }
-
- $this->fail();
- }
-
- /**
- * @covers BankAccount::depositMoney
- */
- public function testBalanceCannotBecomeNegative2()
- {
- try {
- $this->ba->depositMoney(-1);
- }
-
- catch (BankAccountException $e) {
- $this->assertSame(0, $this->ba->getBalance());
-
- return;
- }
-
- $this->fail();
- }
-
- /**
- * @covers BankAccount::getBalance
- * @covers BankAccount::depositMoney
- * @covers BankAccount::withdrawMoney
- */
- 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());
- }
- }