phpunit 的官方文件對如何使用 phpunit 進行了詳細的說明。本人在通讀文件後進行了一些概要提升,
同時摘錄了一些示例 phpunit-demo,便於以後
理解和查閱。
文件較為簡潔,但是也涵蓋了平時使用的基本用法,適合入門使用。
安裝 phpunit
專案安裝
composer require --dev phpunit/phpunit
使用 ./vendor/bin/phpunit
全域性安裝
composer global require --dev phpunit/phpunit
使用 phpunit
快速入門
基本格式
- 測試類命名:
類名 + Test
, egFooClassTest
- 測試方法命名:
test + 方法名
, egtestFoo
也可以使用註釋
@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
來標記。
下列中, 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);
}
}
資料提供器(@dataProvider)
在依賴中,所依賴函式的返回值作為引數傳入測試函式。除此之外,我們也可以用資料提供器
來定義傳入的資料。
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
];
}
}
測試異常(expectException)
需要在測試方法的開始處宣告斷言,然後執行語句。而不是呼叫後再宣告
也可以通過註釋來宣告 @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 錯誤
通過提前新增期望,來使測試正常進行,而不會報出 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
基境
基境就是在測試前需要準備的一系列東西。
比如有的測試需要依賴資料庫的資料,那麼在測試類運作前需要進行資料的準備。
主要有兩個函式 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>
有風險的測試(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
標記待完善的測試
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 來跳過測試
以下的示例中,使用 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
中設定
需要往資料庫中寫入的資料,注意,每次執行這個測試類時,都會執行
- 清空資料庫 TRUNCATE
- 填充資料
如何驗證呢?加一個欄位為 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()
本身沒有返回資料,我們通過樁動作,來完成了測試。
這個在測試依賴於第三方服務的相關程式碼時很管用。
程式碼覆蓋度
白名單檔案(Whitelisting Files)
新增到白名單檔案的檔案或者資料夾,會進行程式碼覆蓋度統計的工作。具體的引數可以看幫助文件
忽略程式碼塊(Ignoring Code Blocks)
我們可以新增部分程式碼不進行覆蓋度統計。通過一些註釋來標記即可
use PHPUnit\Framework\TestCase;
/**
* @codeCoverageIgnore
*/
class Foo
{
public function bar()
{
}
}
class Bar
{
/**
* @codeCoverageIgnore
*/
public function foo()
{
}
}
if (false) {
// @codeCoverageIgnoreStart
print '*';
// @codeCoverageIgnoreEnd
}
exit; // @codeCoverageIgnore
執行方法進行統計(Specifying Covered Methods)
同樣通過新增註釋標記的方法來執行需要覆蓋的方法。
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());
}
}