測試驅動(dòng)的開(kāi)發(fā)和單元測試是確保代碼在經(jīng)過(guò)修改和重大調整之后依然能如我們期望的一樣工作的最新方法。在本文中,您將到如何在模塊、用戶(hù)界面(UI)層對自己的PHP代碼進(jìn)行單元測試。
現在是凌晨 3 點(diǎn)。我們怎樣才能知道自己的代碼依然在工作呢?
Web 應用程序是 24x7 不間斷運行的,因此我的程序是否還在運行這個(gè)問(wèn)題會(huì )在晚上一直困擾我。單元測試已經(jīng)幫我對自己的代碼建立了足夠的信心 —— 這樣我就可以安穩地睡個(gè)好覺(jué)了。
單元測試 是一個(gè)為代碼編寫(xiě)測試用例并自動(dòng)運行這些測試的框架。測試驅動(dòng)的開(kāi)發(fā) 是一種單元測試方法,其思想是應該首先編寫(xiě)測試程序,并驗證這些測試可以發(fā)現錯誤,然后才開(kāi)始編寫(xiě)需要通過(guò)這些測試的代碼。當所有測試都通過(guò)時(shí),我們開(kāi)發(fā)的特性也就完成了。這些單元測試的價(jià)值是我們可以隨時(shí)運行它們 —— 在簽入代碼之前,重大修改之后,或者部署到正在運行的系統之后都可以。
PHP 單元測試
對于 PHP 來(lái)說(shuō),單元測試框架是 PHPUnit2?梢允褂 PEAR 命令行作為一個(gè) PEAR 模塊來(lái)安裝這個(gè)系統:% pear install PHPUnit2。
在安裝這個(gè)框架之后,可以通過(guò)創(chuàng )建派生于 PHPUnit2_Framework_TestCase 的測試類(lèi)來(lái)編寫(xiě)單元測試。
模塊單元測試
我發(fā)現開(kāi)始單元測試最好的地方是在應用程序的業(yè)務(wù)邏輯模塊中。我使用了一個(gè)簡(jiǎn)單的例子:這是一個(gè)對兩個(gè)數字進(jìn)行求和的函數。為了開(kāi)始測試,我們首先編寫(xiě)測試用例,如下所示。
清單 1. TestAdd.php
assertTrue( add( 1, 2 ) == 3 ); }
function test2() { $this->assertTrue( add( 1, 1 ) == 2 ); }
}
?>
這個(gè) TestAdd 類(lèi)有兩個(gè)方法,都使用了 test前綴。每個(gè)方法都定義了一個(gè)測試,這個(gè)測試可以與清單 1 一樣簡(jiǎn)單,也可以十分復雜。在本例中,我們在第一個(gè)測試中只是簡(jiǎn)單地斷定 1 加 2 等于 3,在第二個(gè)測試中是 1 加 1 等于 2。
PHPUnit2 系統定義了 assertTrue() 方法,它用來(lái)測試參數中包含的條件值是否為真。然后,我們又編寫(xiě)了 Add.php 模塊,最初讓它產(chǎn)生錯誤的結果。
清單 2. Add.php
現在運行單元測試時(shí),這兩個(gè)測試都會(huì )失敗。
清單 3. 測試失敗
% phpunit TestAdd.php
PHPUnit 2.2.1 by Sebastian Bergmann.
FF
Time: 0.0031270980834961
There were 2 failures:
1) test1(TestAdd)
2) test2(TestAdd)
FAILURES!!!
Tests run: 2, Failures: 2, Errors: 0, Incomplete Tests: 0.
現在我知道這兩個(gè)測試都可以正常工作了。因此,可以修改 add() 函數來(lái)真正地做實(shí)際的事情了。
現在這兩個(gè)測試都可以通過(guò)了。
清單 4. 測試通過(guò)
% phpunit TestAdd.php
PHPUnit 2.2.1 by Sebastian Bergmann.
..
Time: 0.0023679733276367
OK (2 tests)%
盡管這個(gè)測試驅動(dòng)開(kāi)發(fā)的例子非常簡(jiǎn)單,但是我們可以從中體會(huì )到它的思想。我們首先創(chuàng )建了測試用例,并且有足夠多的代碼讓這個(gè)測試運行起來(lái),不過(guò)結果是錯誤的。然后我們驗證測試的確是失敗的,接著(zhù)實(shí)現了實(shí)際的代碼使這個(gè)測試能夠通過(guò)。
我發(fā)現在實(shí)現代碼時(shí)我會(huì )一直不斷地添加代碼,直到擁有一個(gè)覆蓋所有代碼路徑的完整測試為止。在本文的最后,您會(huì )看到有關(guān)編寫(xiě)什么測試和如何編寫(xiě)這些測試的一些建議。
數據庫測試
在進(jìn)行模塊測試之后,就可以進(jìn)行數據庫訪(fǎng)問(wèn)測試了。數據庫訪(fǎng)問(wèn)測試 帶來(lái)了兩個(gè)有趣的問(wèn)題。首先,我們必須在每次測試之前將數據庫恢復到某個(gè)已知點(diǎn)。其次,要注意這種恢復可能會(huì )對現有數據庫造成破壞,因此我們必須對非生產(chǎn)數據庫進(jìn)行測試,或者在編寫(xiě)測試用例時(shí)注意不能影響現有數據庫的內容。
數據庫的單元測試是從數據庫開(kāi)始的。為了闡述這個(gè)問(wèn)題,我們需要使用下面的簡(jiǎn)單模式。
清單 5. Schema.sql
DROP TABLE IF EXISTS authors;
CREATE TABLE authors (
id MEDIUMINT NOT NULL AUTO_INCREMENT,
name TEXT NOT NULL,
PRIMARY KEY ( id )
);
清單 5 是一個(gè) authors 表,每條記錄都有一個(gè)相關(guān)的 ID。
接下來(lái),就可以編寫(xiě)測試用例了。
清單 6. TestAuthors.php
assertTrue( Authors::delete_all() );
}
function test_insert() {
$this->assertTrue( Authors::delete_all() );
$this->assertTrue( Authors::insert( 'Jack' ) );
}
function test_insert_and_get() {
$this->assertTrue( Authors::delete_all() );
$this->assertTrue( Authors::insert( 'Jack' ) );
$this->assertTrue( Authors::insert( 'Joe' ) );
$found = Authors::get_all();
$this->assertTrue( $found != null );
$this->assertTrue( count( $found ) == 2 );
}
}
?>
這組測試覆蓋了從表中刪除作者、向表中插入作者以及在驗證作者是否存在的同時(shí)插入作者等功能。這是一個(gè)累加的測試,我發(fā)現對于尋找錯誤來(lái)說(shuō)這非常有用。觀(guān)察一下哪些測試可以正常工作,而哪些測試不能正常工作,就可以快速地找出哪些地方出錯了,然后就可以進(jìn)一步理解它們之間的區別。
最初產(chǎn)生失敗的 dblib.php PHP 數據庫訪(fǎng)問(wèn)代碼版本如下所示。
清單 7. dblib.php
getMessage()); }
return $db;
}
public static function delete_all()
{
return false;
}
public static function insert( $name )
{
return false;
}
public static function get_all()
{
return null;
}
}
?>
對清單 8 中的代碼執行單元測試會(huì )顯示這 3 個(gè)測試全部失敗了:
清單 8. dblib.php
% phpunit TestAuthors.php
PHPUnit 2.2.1 by Sebastian Bergmann.
FFF
Time: 0.007500171661377
There were 3 failures:
1) test_delete_all(TestAuthors)
2) test_insert(TestAuthors)
3) test_insert_and_get(TestAuthors)
FAILURES!!!
Tests run: 3, Failures: 3, Errors: 0, Incomplete Tests: 0.
%
現在我們可以開(kāi)始添加正確訪(fǎng)問(wèn)數據庫的代碼 —— 一個(gè)方法一個(gè)方法地添加 —— 直到所有這 3 個(gè)測試都可以通過(guò)。最終版本的 dblib.php 代碼如下所示。
清單 9. 完整的 dblib.php
getMessage()); }
return $db;
}
public static function delete_all()
{
$db = Authors::get_db();
$sth = $db->prepare( 'DELETE FROM authors' );
$db->execute( $sth );
return true;
}
public static function insert( $name )
{
$db = Authors::get_db();
$sth = $db->prepare( 'INSERT INTO authors VALUES (null,?)' );
$db->execute( $sth, array( $name ) );
return true;
}
public static function get_all()
{
$db = Authors::get_db();
$res = $db->query( "SELECT * FROM authors" );
$rows = array();
while( $res->fetchInto( $row ) ) { $rows []= $row; }
return $rows;
}
}
?>
在對這段代碼運行測試時(shí),所有的測試都可以沒(méi)有問(wèn)題地運行,這樣我們就可以知道自己的代碼可以正確工作了。
HTML測試
對整個(gè) PHP 應用程序進(jìn)行測試的下一個(gè)步驟是對前端的超文本標記語(yǔ)言(HTML)界面進(jìn)行測試。要進(jìn)行這種測試,我們需要一個(gè)如下所示的 Web 頁(yè)面。
這個(gè)頁(yè)面對兩個(gè)數字進(jìn)行求和。為了對這個(gè)頁(yè)面進(jìn)行測試,我們首先從單元測試代碼開(kāi)始入手。
清單 10. TestPage.php
get( $url );
$resp = $client->currentResponse();
return $resp['body'];
}
function test_get()
{
$page = TestPage::get_page( 'http://localhost/unit/add.php' );
$this->assertTrue( strlen( $page ) > 0 );
$this->assertTrue( preg_match( '//', $page ) == 1 );
}
function test_add()
{
$page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' );
$this->assertTrue( strlen( $page ) > 0 );
$this->assertTrue( preg_match( '//', $page ) == 1 );
preg_match( '/(.*?)/', $page, $out );
$this->assertTrue( $out[1]=='30' );
}
}
?>
這個(gè)測試使用了 PEAR 提供的 HTTP Client 模塊。我發(fā)現它比內嵌的 PHP Client URL Library(CURL)更簡(jiǎn)單一點(diǎn)兒,不過(guò)也可以使用后者。
有一個(gè)測試會(huì )檢查所返回的頁(yè)面,并判斷這個(gè)頁(yè)面是否包含 HTML。第二個(gè)測試會(huì )通過(guò)將值放到請求的 URL 中來(lái)請求計算 10 和 20 的和,然后檢查返回的頁(yè)面中的結果。
這個(gè)頁(yè)面的代碼如下所示。
清單 11. TestPage.php
" /> +
" /> =
這個(gè)頁(yè)面相當簡(jiǎn)單。兩個(gè)輸入域顯示了請求中提供的當前值。結果 span 顯示了這兩個(gè)值的和。 標記標出了所有區別:它對于用戶(hù)來(lái)說(shuō)是不可見(jiàn)的,但是對于單元測試來(lái)說(shuō)卻是可見(jiàn)的。因此單元測試并不需要復雜的邏輯來(lái)找到這個(gè)值。相反,它會(huì )檢索一個(gè)特定 標記的值。這樣當界面發(fā)生變化時(shí),只要 span 存在,測試就可以通過(guò)。
與前面一樣,首先編寫(xiě)測試用例,然后創(chuàng )建一個(gè)失敗版本的頁(yè)面。我們對失敗情況進(jìn)行測試,然后修改頁(yè)面的內容使其可以工作。結果如下:
清單 12. 測試失敗情況,然后修改頁(yè)面
% phpunit TestPage.php
PHPUnit 2.2.1 by Sebastian Bergmann.
..
Time: 0.25711488723755
OK (2 tests)
%
這兩個(gè)測試都可以通過(guò),這就意味著(zhù)測試代碼可以正常工作。
不過(guò)對 HTML 前端的測試有一個(gè)缺陷:JavaScript。超文本傳輸協(xié)議(HTTP)客戶(hù)機代碼對頁(yè)面進(jìn)行檢索,但是卻沒(méi)有執行 JavaScript。因此如果我們在 JavaScript 中有很多代碼,就必須創(chuàng )建用戶(hù)代理級的單元測試。我發(fā)現實(shí)現這種功能的最佳方法是使用 Microsoft® Internet Explorer® 內嵌的自動(dòng)化層功能。通過(guò)使用 PHP 編寫(xiě)的 Microsoft Windows® 腳本,可以使用組件對象模型(COM)接口來(lái)控制 Internet Explorer,讓它在頁(yè)面之間進(jìn)行導航,然后使用文檔對象模型(DOM)方法在執行特定用戶(hù)操作之后查找頁(yè)面中的元素。
這是我了解的對前端 JavaScript 代碼進(jìn)行單元測試的惟一一種方法。我承認它并不容易編寫(xiě)和維護,這些測試即使在對頁(yè)面稍微進(jìn)行改動(dòng)時(shí)也很容易遭到破壞。
編寫(xiě)哪些測試以及如何編寫(xiě)這些測試
在編寫(xiě)測試時(shí),我喜歡覆蓋以下情況:
所有正面測試
這組測試可以確保所有的東西都如我們期望的一樣工作。
所有負面測試
逐一使用這些測試,從而確保每個(gè)失效或異常情況都被測試到了。
正面序列測試
這組測試可以確保按照正確順序的調用可以像我們期望的一樣工作。
負面序列測試
這組測試可以確保當不按正確順序進(jìn)行調用時(shí)就會(huì )失敗。
負載測試
在適當情況下,可以執行一小組測試來(lái)確定這些測試的性能在我們期望的范圍之內。例如,2,000 次調用應該在 2 秒之內完成。
資源測試
這些測試確保應用編程接口(API)可以正確地分配并釋放資源 —— 例如,連續幾次調用打開(kāi)、寫(xiě)入以及關(guān)閉基于文件的 API,從而確保沒(méi)有文件依然是被打開(kāi)的。
回調測試
對于具有回調方法的 API 來(lái)說(shuō),這些測試可以確保如果沒(méi)有定義回調函數,代碼可以正常運行。另外,這些測試還可以確保在定義了回調函數但是這些回調函數操作有誤或產(chǎn)生異常時(shí),代碼依然可以正常運行。
這是有關(guān)單元測試的幾點(diǎn)想法。有關(guān)如何編寫(xiě)單元測試,我也有幾點(diǎn)建議:
1.不要使用隨機數據
盡管在一個(gè)界面中產(chǎn)生隨機數據看起來(lái)貌似一個(gè)好主意,但是我們要避免這樣做,因為這些數據會(huì )變得非常難以調試。如果數據是在每次調用時(shí)隨機生成的,那么就可能產(chǎn)生一次測試時(shí)出現了錯誤而另外一次測試卻沒(méi)有出現錯誤的情況。如果測試需要隨機數據,可以在一個(gè)文件中生成這些數據,然后每次運行時(shí)都使用這個(gè)文件。采用這種方法,我們就獲得了一些 “噪音” 數據,但是仍然可以對錯誤進(jìn)行調試。
2.分組測試
我們很容易累積起數千個(gè)測試,需要幾個(gè)小時(shí)才能執行完。這沒(méi)什么問(wèn)題,但是對這些測試進(jìn)行分組使我們可以快速運行某組測試并對主要關(guān)注的問(wèn)題進(jìn)行檢查,然后晚上運行完整的測試。
3.編寫(xiě)穩健的 API 和穩健的測試
編寫(xiě) API 和測試時(shí)要注意它們不能在增加新功能或修改現有功能時(shí)很容易就會(huì )崩潰,這一點(diǎn)非常重要。這里沒(méi)有通用的絕招,但是有一條準則是那些 “振蕩的” 測試(一會(huì )兒失敗,一會(huì )兒成功,反復不停的測試)應該很快地丟棄。
結束語(yǔ)
單元測試對于工程師來(lái)說(shuō)意義重大。它們是敏捷開(kāi)發(fā)過(guò)程(這個(gè)過(guò)程非常強調編碼的作用,因為文檔需要一些證據證明代碼是按照規范進(jìn)行工作的)的一個(gè)基礎。單元測試就提供了這種證據。這個(gè)過(guò)程從單元測試開(kāi)始入手,這定義了代碼應該 實(shí)現但目前尚未 實(shí)現的功能。因此,所有的測試最初都會(huì )失敗。然后當代碼接近完成時(shí),測試就通過(guò)了。當所有測試全部通過(guò)時(shí),代碼也就變得非常完善了。
我從來(lái)沒(méi)有在不使用單元測試的情況下編寫(xiě)大型代碼或修改大型或復雜的代碼塊。我通常都是在修改代碼之前就為現有代碼編寫(xiě)了單元測試,這樣可以確保自己清楚在修改代碼時(shí)破壞了什么(或者沒(méi)有破壞什么)。這為我對自己提供給客戶(hù)的代碼提供了很大的信心,相信它們正在正確運行 —— 即便是在凌晨 3 點(diǎn)。
文章來(lái)源于領(lǐng)測軟件測試網(wǎng) http://kjueaiud.com/