有的時候?qū)幵父跺X讓你一周在床上待著,也不想讓你用這周剩下的時間去調(diào)試你在周一所寫的代碼。 --丹·所羅門
做正確的事,比把事情做正確更為重要。
當明確需要做何事后,再通過事先編寫單元測試來準確表達我們將要實現(xiàn)的功能,是相當具有指導(dǎo)意義的。你會發(fā)現(xiàn)接下來你的開發(fā)歷程就是:單元測試-設(shè)計-重構(gòu),而且這種正向循環(huán)是很有創(chuàng)造性的,并且進行到一定程度后會慢慢體會到浮現(xiàn)式設(shè)計的樂趣。
關(guān)于測試驅(qū)動開發(fā)TDD,有很多資料已進行了說明,這里不再贅述。
如果還沒了解PHPUnit,可先閱讀:PHPUnit 手冊
如果還沒了解PHPUnit,可先閱讀:PHPUnit 手冊
如果還沒了解PHPUnit,可先閱讀:PHPUnit 手冊
在編寫代碼前,先寫測試代碼,更容易提高 關(guān)注點 。
因為,在開發(fā)過程中, 大多時候會被外界打斷(如需求溝通、線上問題處理、臨時會議等),而通過單元測試則可以讓你“幾乎忘卻需要做什么”的情況下重新讓你回到之前的狀態(tài),特別在并行開發(fā)多個不同項目的需求時尤其重要。
除此之外,遵循“紅-綠-重構(gòu)”這樣的流程,我們可以在更高的層面關(guān)注需要實現(xiàn)的功能需求,并自頂而下地進行設(shè)計優(yōu)化,精益代碼。
首先應(yīng)該意識到,測試代碼和生產(chǎn)代碼一樣重要。其次,測試代碼也應(yīng)該和生產(chǎn)代碼一樣被同步維護更新,這樣才能保持生氣,更大地發(fā)揮作用。只有當不斷地對測試的代碼進行修修補被,我們才能保持自動化測試這張“安全網(wǎng)”常新。
這個模式也可以理解成:“當... 做...應(yīng)該...”。其中,構(gòu)造包括測試環(huán)境的搭建、測試數(shù)據(jù)前期的準備;操作是指對被測試對象的調(diào)用, 以及被測試對象之間的通信和協(xié)助交互;最后檢驗則是對業(yè)務(wù)規(guī)則的斷言、對功能需求的驗證。
我們推薦在各自的項目代碼中平行編寫單元測試,并逐漸完善、保持同步。以下是進行單元測試的參考。
Api接口層,是我們后臺開發(fā)的主要切入點,也是直接對外提供服務(wù)的入口,屬于更高層次的概念并擁有指定的業(yè)務(wù)功能,更是后臺開發(fā)的關(guān)注點。所以在對新接口進行開發(fā)前,編寫單元測試是非常有意義的。
為了可以自動生成測試代碼,我們可以先簡單定義好接口的函數(shù)簽名(以獲取用戶基本信息接口為例):
//$ vim ./Demo/Api/User.php
<?php
class Api_User extends PhalApi_Api {
public function getBaseInfo() {
}
}
隨后,自動生成測試代碼骨架:
$ mkdir ./Demo/Tests/Api -p
$ cd ./Demo/Tests/Api
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Api/User.php Api_User ./Public/init.php
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Api/User.php Api_User ./Public/init.php > ./Demo/Tests/Api/Api_User_Test.php
根據(jù)接口的需要,驗證接口返回的格式,以及業(yè)務(wù)數(shù)據(jù)的正確性。
//$ vim ./Demo/Tests/Api/Api_User_Test.php
/**
* @group testGetBaseInfo
*/
public function testGetBaseInfo()
{
//Step 1. 構(gòu)建請求URL
$str = 'service=User.GetBaseInfo&user_id=1';
//Step 2. 執(zhí)行請求
$rs = PhalApi_Helper_TestRunner::go($url);
//Step 3. 驗證
$this->assertNotEmpty($rs);
$this->assertArrayHasKey('code', $rs);
$this->assertArrayHasKey('msg', $rs);
$this->assertArrayHasKey('info', $rs);
$this->assertEquals(0, $rs['code']);
$this->assertEquals('dogstar', $rs['info']['name']);
$this->assertEquals('oschina', $rs['info']['note']);
}
上面的驗證意思簡單明了,結(jié)合 構(gòu)造-操作-檢驗(BUILD-OPERATE-CHECK)模式 加以說明一下。
//Step 1. 構(gòu)建請求URL
$str = 'service=User.GetBaseInfo&user_id=1';
此參數(shù)即對應(yīng)接口請求的URL參數(shù),我們將此參數(shù)追加在接口入口并在瀏覽器打開可以得到同樣的接口執(zhí)行效果。但這樣的好處更在于通過單元測試幫我們記住了各種接口測試的業(yè)務(wù)場景。而不再是像以前那樣打開N個瀏覽器窗口人工進行調(diào)試,也不用像以前那樣苦苦尋找瀏覽器記錄。
如果接口需要POST數(shù)據(jù),或者其他更多參數(shù),可以使用$params來傳遞更多參數(shù),一如:
//Step 1. 構(gòu)建請求URL
$str = 'service=User.GetBaseInfo&user_id=1';
$params = array(); //更多參數(shù)
//Step 2. 執(zhí)行請求
$rs = PhalApi_Helper_TestRunner::go($url, $params); //通過第二個參數(shù),傳送更多參數(shù)
這里的操作,顯然就是對應(yīng)我們接口的調(diào)用。簡單地如:
//Step 2. 執(zhí)行請求
$rs = PhalApi_Helper_TestRunner::go($url);
這樣,便可以在服務(wù)端模擬進行一次接口的請求調(diào)度,注意這里是在服務(wù)端進行的接口請求,而不是客戶端。
此外,如果需要傳遞更多參數(shù),可以參考前面的示例。這里簡單補充一下PhalApi_Helper_TestRunner測試輔助類的接口簽名說明:
<?php
class PhalApi_Helper_TestRunner {
/**
* @param string $url 請求的鏈接
* @param array $param 額外POST的數(shù)據(jù)
* @return array 接口的返回結(jié)果
*/
public static function go($url, $params = array()) {
... ...
在對接口返回的結(jié)果中,我們可以這樣依次進行正確性的驗證:
//Step 3. 驗證
$this->assertNotEmpty($rs);
$this->assertArrayHasKey('code', $rs);
$this->assertArrayHasKey('msg', $rs);
$this->assertArrayHasKey('info', $rs);
$this->assertEquals(0, $rs['code']);
$this->assertEquals('dogstar', $rs['info']['name']);
$this->assertEquals('oschina', $rs['info']['note']);
由于測試環(huán)境的數(shù)據(jù)變動頻繁,所以我們可以針對個別的接口進行更精確的驗證,而對類似列表獲取這樣的大批量的數(shù)據(jù),則校驗其結(jié)構(gòu)格式。
除此之外,還有一種情況也是需要納入檢驗,即除了上面的正常請求情況下的 異常請求 。
接下來的即是之前文檔里面所說的單元測試執(zhí)行和接口開發(fā),此處略。
下面繼續(xù)簡單補充一下之前沒談及到的Domain層和Model層的單元測試。
顯然,這兩層的開發(fā),已經(jīng)在前面的接口測試驅(qū)動開發(fā)的指導(dǎo)下很好地完成了?,F(xiàn)在可以快速追加對這兩層的單元測試。得益于我們的生成測試骨架的腳本,操作如下:
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Domain/User.php Domain_User > ./Demo/Tests/Domain/Domain_User_Test.php
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Model/User.php Model_User > ./Demo/Tests/Model/Model_User_Test.php
接著,修改一下測試環(huán)境 test_env.php的引用路徑:
//$ vim ./Demo/Tests/Domain/Domain_User_Test.php
//$ vim ./Demo/Tests/Model/Model_User_Test.php
require_once dirname(__FILE__) . '/../test_env.php';
各自完善一下單元測試:
//$ vim ./Demo/Tests/Domain/Domain_User_Test.php
/**
* @group testGetBaseInfo
*/
public function testGetBaseInfo()
{
$userId = '1';
$rs = $this->domainUser->getBaseInfo($userId);
$this->assertArrayHasKey('id', $rs);
$this->assertArrayHasKey('name', $rs);
$this->assertArrayHasKey('note', $rs);
$this->assertEquals('dogstar', $rs['name']);
}
執(zhí)行一下:
$ phpunit ./Demo/Tests/Domain/Domain_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.
.
Time: 49 ms, Memory: 6.25Mb
OK (1 test, 4 assertions)
Model層的單元測試類似,不再贅述。
到目前為止,我們有了如下的產(chǎn)品代碼:
dogstar@ubuntu:Demo$ tree
.
├── Api
│ └── User.php
├── Domain
│ └── User.php
├── Model
│ └── User.php
并擁有了與之平行對應(yīng)的單元測試:
dogstar@ubuntu:Tests$ tree
.
├── Api
│ └── Api_User_Test.php
├── Domain
│ └── Domain_User_Test.php
├── Model
│ └── Model_User_Test.php
└── test_env.php
這樣是一個很好的開始,但若我們每次測試都分別調(diào)用三次這些不同層次的單元測試,顯然有點不科學(xué)。所以,利用PHPUnit的配置文件,我們可以輕松管理我們的測試套件,如:
dogstar@ubuntu:Tests$ vim ./phpunit_user_getbaseinfo.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
...
<testsuites>
<testsuite name="Test Suite">
<file>./Api/Api_User_Test.php</file>
<file>./Domain/Domain_User_Test.php</file>
<file>./Model/Model_User_Test.php</file>
</testsuite>
</testsuites>
</phpunit>
啊哈!終于,當需要調(diào)用這些分布在不同目錄位置的單元測試時,只需要這么簡單的一行命令:
dogstar@ubuntu:Tests$ phpunit -c ./phpunit_user_getbaseinfo.xml
PHPUnit 4.3.4 by Sebastian Bergmann.
.....
Time: 54 ms, Memory: 7.25Mb
OK (5 tests, 28 assertions)
上面的過程,細節(jié)較多,而且需要實際操作的部分也比較多。對于之前沒有接觸過單元測試這塊的同學(xué),可能會有點迷茫,對于不愿意接受單元測試的同學(xué)來說更加枯燥。
然而,然而。 當我們把越痛苦的事情越早完成后,我們后面就順暢多了。正如在某一次培訓(xùn)中的某一位敏捷開發(fā)的專家所說的: 要逐步對小問題做優(yōu)化,而不是要等到大問題到來時再做變革 。
這里不就理論回答,而是以我個人的經(jīng)歷來簡單說明。
首先,正如上面所說的,單元測試幫你很好地記住并整理了各種接口測試的場景,而不用再像以前那樣打開N個瀏覽器窗口逐個人工校對。
其次,在單元測試的論證下我們可以更有信心地跟測試說、跟產(chǎn)品說、跟發(fā)布說我們的代碼沒問題,因為我們通過嚴格的單元測試,而不是人為主觀上的想當然應(yīng)該不會有問題吧。
最后,也是最重要的,在后期的接口升級、改動和維護中,單元測試再一次為我們提供了保護,猶如一張安全網(wǎng),涵蓋我們改動的每一處代碼。與此同時,對于重構(gòu)也亦然。
但單元測試所帶給你的,不僅僅是上面所說的簡單這幾點。更多地完全不一樣的開發(fā)歷程,而其中滋味和令人興奮的體現(xiàn),只有當你親自去嘗試才會明白其中滋味。So, try it by yourself.
更多建議: