PHPUnit單元測試
一、概述
1. 什麼是單元測試?
- 【百度百科】單元測試是對軟件中的最小可測單元進行檢查和驗證。
- 是開發者編寫的一小段代碼,用於檢驗被測代碼的一個很小的、很明確的功能是否正確。
2. 作用是什麼?
- 【廢話】檢查軟件、程序的可行性,穩定性。
- 通過單元測試能夠避免在迭代、升級等過程中,引起重複的、多餘的問題。
- 避免在別人修改代碼的時候,影響到你的邏輯
3. 哪些程序需要寫單元測試(PHP)?
- 【理想】理想的單元測試應當覆蓋程序中所有可能的路徑,包括正確的和錯誤的路徑,個單元測試通常覆蓋一個函數或方法中的一個特定路徑。
- 【現實】model、helper、controller中的函數必須測試、路徑覆蓋到所有可能性
二、PHPUnit的安裝與集成CI框架
- 略。。。。。後續再補
三、PHPUnit的使用
編寫測試用例
- 測試的依賴關係 @depends
PHPUnit支持對測試方法之間的顯式依賴關係進行聲明。這種依賴關係並不是定義在測試方法的執行順序中,而是允許生產者(producer)返回一個測試基境(fixture)的實例,並將此實例傳遞給依賴於它的消費者(consumer)們。
<?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); } } ?>
- 默認情況下,生產者所產生的返回值將“原樣”傳遞給相應的消費者。這意味着,如果生產者返回的是一個對象,那麼傳遞給消費者的將是一個指向此對象的引用。如果需要傳遞對象的副本而非引用,則應當用 @depends clone 替代 @depends。
- 數據供給器 @dataProvider 測試方法可以接受任意參數。用 @dataProvider 標註來指定使用哪個數據供給器方法。 數據供給器方法必須聲明爲 public,其返回值要麼是一個數組,其每個元素也是數組;要麼是一個實現了 Iterator 接口的對象,在對它進行迭代時每步產生一個數組。每個數組都是測試數據集的一部分,將以它的內容作爲參數來調用測試方法。
<?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)
);
}
}
?>
也可以是這樣:
<?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)
);
}
}
?>
對PHP錯誤進行測試
默認情況下,PHPUnit 將測試在執行中觸發的 PHP 錯誤、警告、通知都轉換爲異常。利用這些異常,就可以,比如說,預期測試將觸發 PHP 錯誤
<?php
class ExpectedErrorTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException PHPUnit_Framework_Error
*/
public function testFailingInclude()
{
include 'not_existing_file.php';
}
}
?>
測試
phpunit -d error_reporting=2 ExpectedErrorTest
PHPUnit 5.2.0 by Sebastian Bergmann and contributors.
.
Time: 0 seconds, Memory: 5.25Mb
OK (1 test, 1 assertion)
命令行測試執行器
說明
PHPUnit 測試執行器可通過phpunit 調用,例如在CI中:
tongkundeMacBook-Pro:www tongkun$ cd tests/
tongkundeMacBook-Pro:tests tongkun$ phpunit
PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
............. 13 / 13 (100%)
Time: 195 ms, Memory: 17.50Mb
OK (13 tests, 8 assertions)
說明:
先進入測試的根目錄,執行phpunit 命令,後面可跟具體的目錄或文件,也可不跟,如果沒有則會對當前目錄的所有文件執行單元測試,對於每個測試的運行,PHPUnit命令行工具會輸出一個字符來指示進展:
- . 當測試陳宮時輸出
- F 當測試方法運行過程中一個斷言失敗時輸出,例如一個失敗的assertEquals()調用
- E 當測試方法運行過程中產生一個錯誤時輸出,錯誤是指意料之外的異常(exception)或者PHP錯誤
- R 當測試被標記有風險時輸出
- S 當測試跳出時輸出
- I 當測試被標記不完整或爲實現時輸出
常用命令行選項
- --coverage-clover:爲運行的測試生成帶有代碼覆蓋率信息的 XML 格式的日誌文件
- --coverage-html:生成 HTML 格式的代碼覆蓋率報告
- --coverage-php:生成一個序列化後的 PHP_CodeCoverage 對象,此對象含有代碼覆蓋率信息
- --log-json:生成 JSON 格式的日誌文件
- --filter:只運行名稱與給定模式匹配的測試。如果模式未閉合包裹於分隔符,PHPUnit 將用 / 分隔符對其進行閉合包裹
tongkundeMacBook-Pro:tests tongkun$ phpunit --filter 'WelcomeTest::testTest' - PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
- ... 3 / 3 (100%)
- Time: 148 ms, Memory: 16.75Mb
- OK (3 tests, 3 assertions)
這樣測試的就是WelcomeTest類中的testTest函數,過濾模式的例子有很多,詳見文檔官方- --colors:使用彩色輸出。Windows下,用 ANSICON 或 ConEmu。
本選項有三個可能的值:
never: 完全不使用彩色輸出。當未使用 --colors 選項時,這是默認值。
auto: 如果當前終端不支持彩色、或者輸出被管道輸出至其他命令、或輸出被重定向至文件時,不使用彩色輸出,其餘情況使用彩色。
always: 總是使用彩色輸出,即使當前終端不支持彩色、輸出被管道輸出至其他命令、或輸出被重定向至文件。
當使用了 --colors 選項但未指定任何值時,將選擇 auto 做爲其值。 - --stop-on-error:首次錯誤出現後停止執行。
- --stop-on-failure:首次錯誤或失敗出現後停止執行。
- --stop-on-risky:首次碰到有風險的測試時停止執行。
- --stop-on-risky:首次碰到有風險的測試時停止執行。
- --stop-on-incomplete首次碰到不完整的測試時停止執行。
- --repeat:將測試重複運行指定次數。
tongkundeMacBook-Pro:tests tongkun$ phpunit --repeat 10 - PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
- ............................................................... 63 / 130 ( 48%)
- ............................................................... 126 / 130 ( 96%)
- .... 130 / 130 (100%)
- Time: 456 ms, Memory: 26.25Mb
- OK (130 tests, 80 assertions)
- --tap:使用 Test Anything Protocol (TAP) 報告測試進度
tongkundeMacBook-Pro:tests tongkun$ phpunit --tap - TAP version 13
- ok 1 - CI_Unit_Test_class_Test::test_CI_Unit_Test_Class
- ok 2 - SomeControllerTest::testWelcomeController
- ok 3 - WelcomeTest::testIndex
- ok 4 - WelcomeTest::testTest
- ok 5 - WelcomeTest::testOutput
- ok 6 - WelcomeTest::testTest1
- ok 7 - WelcomeTest::testTest2
- ok 8 - HelperTest::testSampleFunction
- ok 9 - SomeLibTest::testMethod
- ok 10 - M_user_masterTest::testSelect
- ok 11 - M_user_masterTest::testInsert
- ok 12 - PHPTest::testFunctionJsonEncode
- ok 13 - PHPTest::testPhpVersion
- 1..13
- --configuration, -c:從 XML 文件中讀取配置信息。更多細節請參見附錄 C。
如果 phpunit.xml 或 phpunit.xml.dist (按此順序)存在於當前工作目錄並且未使用 --configuration,將自動從此文件中讀取配置。
tongkundeMacBook-Pro:tests tongkun$ phpunit --configuration phpunit.xml - PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
- ............. 13 / 13 (100%)
- Time: 209 ms, Memory: 17.50Mb
- OK (13 tests, 8 assertions)
- --no-configuration:忽略當前工作目錄下的 phpunit.xml 與 phpunit.xml.dist。
四、基境(fixture)
什麼是基境?
“基境”就是編寫代碼來將整個場景設置成某個已知的狀態,並在測試結束後將其復原到初始狀態。這個已知的狀態稱爲測試的 基境(fixture)。
基境的建立
PHPUnit 支持共享建立基境的代碼。在運行某個測試方法前,會調用一個名叫 setUp() 的模板方法。setUp() 是創建測試所用對象的地方。當測試方法運行結束後,不管是成功還是失敗,都會調用另外一個名叫 tearDown() 的模板方法。tearDown() 是清理測試所用對象的地方。
<?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() 模板方法將分別在測試用例類的第一個測試運行之前和測試用例類的最後一個測試運行之後調用。基境共享可以在共享數據庫連接時使用;
五、組織測試
用文件系統來編排測試套件
例如:
phpunit controllers/WelcomeTest.php
用 XML 配置來編排測試套件
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
//配置文件
colors="true" //顏色
stopOnFailure="false" //出錯後是否終止
bootstrap="../application/third_party/CIUnit/bootstrap_phpunit.php"> //bootstrap 地址
<php>
<server name="SERVER_NAME" value="http://www.nyhdev.com" />
</php>
<testsuites>
//測試套件
<testsuite name="ControllerTests">
<directory>controllers</directory> //要測試的目錄
</testsuite>
<testsuite name="HelperTests">
<directory suffix=".php">helpers</directory>
</testsuite>
<testsuite name="LibTests">
<directory suffix=".php">libs</directory>
</testsuite>
<testsuite name="ModelTests">
<directory suffix=".php">models</directory>
</testsuite>
<testsuite name="SystemTests">
<directory suffix=".php">system</directory>
</testsuite>
</testsuites>
<filter>
<blacklist>
<directory>vendor</directory>
<directory>libs</directory>
</blacklist>
<whitelist>
<directory>controllers</directory>
<directory>fixtures</directory>
<directory>models</directory>
<directory>helpers</directory>
</whitelist>
</filter>
</phpunit>
六、有風險的測試
無用測試
PHPUnit 可以更嚴格對待事實上不測試任何內容的測試。此項檢查可以用命令行選項 --report-useless-tests 或在 PHPUnit 的 XML 配置文件中設置 beStrictAboutTestsThatDoNotTestAnything="true" 來啓用。
在啓用本項檢查後,如果某個測試未進行任何斷言,它將被標記爲有風險。仿件對象中的預期和諸如 @expectedException 這樣的標註同樣視爲斷言。
測試執行期間產生的輸出
PHPUnit 可以更嚴格對待測試執行期間產生的輸出。 此項檢查可以用命令行選項 --disallow-test-output 或在 PHPUnit 的 XML 配置文件中設置 beStrictAboutOutputDuringTests="true" 來啓用。
在啓用本項檢查後,如果某個測試產生了輸出,例如,在測試代碼或被測代碼中調用了 print,它將被標記爲有風險。
七、未完成的測試與跳過的測試
未完成的測試
開始寫新的測試用例類時,可能想從寫下空測試方法開始,比如:
public function testSomething()
{
}
假如把成功的測試視爲綠燈、測試失敗視爲紅燈,那麼還額外需要黃燈來將測試標記爲未完成或尚未實現。PHPUnit_Framework_IncompleteTest 是一個標記接口,用於將測試方法拋出的異常標記爲測試未完成或目前尚未實現而導致的結果。PHPUnit_Framework_IncompleteTestError 是這個接口的標準實現。
例如:我們有一個測試文件,contrllers/WelcomeTest.php,其中有一個測試方法,通過在測試方法中調用markTestIncomplete()將這個測試標記爲未完成。
public function testTest() {
$this->assertTrue(true,'這裏可以正常工作');
$this->markTestIncomplete('此測試尚未實現');
}
在PHPUnit命令行測試執行器中輸出,未完成的測試標記爲1, 如下:
localhost:tests tongkun$ phpunit
PHPUnit 5.0.0 by Sebastian Bergmann and contributors.
R..I...RRRR.. 13 / 13 (100%)
Time: 187 ms, Memory: 17.75Mb
OK, but incomplete, skipped, or risky tests!
Tests: 13, Assertions: 8, Incomplete: 1, Risky: 5.
跳過測試
如上,如果有些測試需要某些環境或者配置才能完成,則可選擇跳過,通過調用 markTestSkipped() 方法來測試
用 @requires 來跳過測試
除了上述方法,還可以用 @requires 標註來表達測試用例的一些常見前提條件。 事例:
例 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 擴展的測試
}
?>
要求安裝mysqli苦戰和php 5.3 才能執行
#常用斷言
前邊廢話一篇,終於到了關鍵的斷言部分,斷言可以說是單元測試的核心,通過斷言的校驗,保證程序的正確運行,並輸出正確的值。
- assertArrayHasKey()
assertArrayHasKey(mixed $key, array $array[, string $message = ''])
當 $array 不包含 $key 時報告錯誤,錯誤訊息由 $message 指定。
assertArrayNotHasKey() 是與之相反的斷言,接受相同的參數。 - assertContains()
assertContains(mixed $needle, Iterator|array $haystack[, string $message = ''])
當 $needle 不是 $haystack的元素時報告錯誤,錯誤訊息由 $message 指定。
assertNotContains() 是與之相反的斷言,接受相同的參數。
assertContains(string $needle, string $haystack[, string $message = '', boolean $ignoreCase = FALSE])
當 $needle 不是 $haystack 的子字符串時報告錯誤,錯誤訊息由 $message 指定。 - assertContainsOnly()
assertContainsOnly(string $type, Iterator|array $haystack[, boolean $isNativeType = NULL, string $message = ''])
當 $haystack 並非僅包含類型爲 $type 的變量時報告錯誤,錯誤訊息由 $message 指定。
$isNativeType 是一個標誌,用來表明 $type 是否是原生 PHP 類型。 - assertEmpty()
assertEmpty(mixed $actual[, string $message = ''])
當 $actual 非空時報告錯誤,錯誤訊息由 $message 指定。
assertNotEmpty() 是與之相反的斷言,接受相同的參數。
assertAttributeEmpty() 和 assertAttributeNotEmpty() 是便捷包裝(convenience wrapper),可以應用於某個類或對象的某個 public、protected 或 private 屬性。 - assertEquals()
assertEquals(mixed $expected, mixed $actual[, string $message = ''])
當兩個變量 $expected 和 $actual 不相等時報告錯誤,錯誤訊息由 $message 指定。
assertNotEquals() 是與之相反的斷言,接受相同的參數。
注意特定類型的比較(浮點型等),詳見文檔 - assertFalse()
assertFalse(bool $condition[, string $message = ''])
當 $condition 爲 TRUE 時報告錯誤,錯誤訊息由 $message 指定。
assertNotFalse() 是與之相反的斷言,接受相同的參數。 - assertNull()
assertNull(mixed $variable[, string $message = ''])
當 $actual 不是 NULL 時報告錯誤,錯誤訊息由 $message 指定。
assertNotNull() 是與之相反的斷言,接受相同的參數。 - assertRegExp() assertRegExp(string $pattern, string $string[, string $message = ''])
當 $string 不匹配於正則表達式 $pattern 時報告錯誤,錯誤訊息由 $message 指定。
assertNotRegExp() 是與之相反的斷言,接受相同的參數。 - assertStringMatchesFormat()
assertStringMatchesFormat(string $format, string $string[, string $message = ''])
當 $string 不匹配於 $format 定義的格式時報告錯誤,錯誤訊息由 $message 指定。
assertStringNotMatchesFormat() 是與之相反的斷言,接受相同的參數。 - assertSame() assertSame(mixed $expected, mixed $actual[, string $message = ''])
當兩個變量 $expected 和 $actual 的值與類型不完全相同時報告錯誤,錯誤訊息由 $message 指定。
assertNotSame() 是與之相反的斷言,接受相同的參數。
assertAttributeSame() 和 assertAttributeNotSame() 是便捷包裝(convenience wrapper),以某個類或對象的某個 public、protected 或 private 屬性作爲實際值來進行比較。 - assertTrue()
assertTrue(bool $condition[, string $message = ''])
當 $condition 爲 FALSE 時報告錯誤,錯誤訊息由 $message 指定。
assertNotTrue() 是與之相反的斷言,接受相同的參數。