很多人會說,其實一開始我內心是想做單元測試(unit testing)的,但時間久了,也就不想了。
要想通過PHP編程成為技術領域的專家,其實功夫在PHP之外。數據庫至少得看幾本書,xml至少得看一本書,單元測試至少得看一本書,軟件工程至少看一本,數據結構與算法至少看一本,*nix至少得看一本,Web服務器至少看一本,佛經得看一本,道德經得看一本,易經可能也得看一本,等等,不停的看下去。
概念
要寫單元測試,必須要有一些基本概念。這些概念PHP是不會教給你的。
我們先從百度百科中吸取一點營養。
工廠在組裝一台電視機之前,會對每個元件都進行測試,這就是單元測試。
單元測試,是指對軟件中的最小可測試單元進行檢查和驗證。對於單元測試中單元的含義,一般來說,要根據實際情況去判定其具體含義,如C語言中單元指一個函數,Java里單元指一個類,圖形化的軟件中可以指一個窗口或一個菜單等。總的來說,單元就是人為規定的最小的被測功能模塊。單元測試是在軟件開發過程中要進行的最低級別的測試活動,軟件的獨立單元將在與程序的其他部分相隔離的情況下進行測試。
單元測試是由程序員自己來完成,最終受益的也是程序員自己。程序員有責任編寫功能代碼,同時也就有責任為自己的代碼編寫單元測試。執行單元測試,就是為了證明這段代碼的行為和我們期望的一致。
--百度百科
安裝
PHPUnit目前穩定版已到了5.0,網上很多的姿勢都已失效。比如,發行方式,以前都是用Pear進行安裝,現在呢,直接是Phar包或者Composer的方式依賴安裝。
2015.10.15注
經測試,當前Netbeans 8.0.2 只支持到PHPUnit 4.7.7。
wget https://phar.phpunit.de/phpunit.phar
➜ chmod +x phpunit.phar
➜ sudo mv phpunit.phar /usr/local/bin/phpunit
➜ phpunit --version
{
"require-dev": {
"phpunit/phpunit": "4.6.*"
}
}
但另一個對初學者很關鍵的東東phpunit-skelgen就沒有包含在這個包里,而且很難找到下載地址:https://phar.phpunit.de/phpunit-skelgen.phar
wget https://phar.phpunit.de/phpunit-skelgen.phar
chmod +x phpunit-skelgen.phar
mv phpunit-skelgen.phar /usr/local/bin/phpunit-skelgen
解決的問題
在開發過程中,當需要對軟件的內部結構進行更改時,你實際上是要在不影響其可見行為的情況下讓它更加容易理解、更加易於修改,測試套件對於安全地進行這些所謂的重構而言是非常寶貴的。否則,你可能在重組過程中將系統搞壞而不自知。
在使用單元測試來確認重構的轉換步驟中確實保持原有行為並且沒有引入錯誤時,以下情況有助於改進項目的編碼與設計:
-
所有單元測試均正確運行。
-
代碼傳達其設計原則。
-
代碼沒有冗余。
-
代碼所包含的類和方法的數量降至最低。
當需要向系統內添加新的功能時,首先為其編寫測試。然后,當測試能夠正常運行就標志着開發完成了。
優點
1、它是一種驗證行為。
程序中的每一項功能都是測試來驗證它的正確性。它為以后的開發提供支援。就算是開發后期,我們也可以輕松的增加功能或更改程序結構,而不用擔心這個過程中會破壞重要的東西。而且它為代碼的重構提供了保障。這樣,我們就可以更自由的對程序進行改進。
2、它是一種設計行為。
編寫單元測試將使我們從調用者觀察、思考。特別是先寫測試(test-first),迫使我們把程序設計成易於調用和可測試的,即迫使我們解除軟件中的耦合。
3、它是一種編寫文檔的行為。
單元測試是一種無價的文檔,它是展示函數或類如何使用的最佳文檔。這份文檔是可編譯、可運行的,並且它保持最新,永遠與代碼同步。
4、它具有回歸性。
自動化的單元測試避免了代碼出現回歸,編寫完成之后,可以隨時隨地的快速運行測試。
實踐
什么時候測試?
單元測試越早越好,早到什么程度?
極限編程(Extreme Programming,或簡稱XP)講究TDD,即測試驅動開發,先編寫測試代碼,再進行開發。在實際的工作中,可以不必過分強調先什么后什么,重要的是高效和感覺舒適。
從經驗來看,先編寫產品函數的框架,然后編寫測試函數,針對產品函數的功能編寫測試用例,然后編寫產品函數的代碼,每寫一個功能點都運行測試,隨時補充測試用例。
所謂先編寫產品函數的框架,是指先編寫函數空的實現,有返回值的直接返回一個合適值,編譯通過后再編寫測試代碼,這時,函數名、參數表、返回類型都應該確定下來了,所編寫的測試代碼以后需修改的可能性比較小。
由誰測試?
單元測試與其他測試不同,單元測試可看作是編碼工作的一部分,應該由程序員完成,也就是說,經過了單元測試的代碼才是已完成的代碼,提交產品代碼時也要同時提交測試代碼。測試部門可以作一定程度的審核。
請一定要看完官方文檔:https://phpunit.de/manual/current/zh_cn/index.html。
要進行充分的單元測試,一般來說應專門編寫測試代碼,並與產品代碼隔離。但對於初學者來說,總是會有點別扭,因為感覺額外做了很多工作,影響開發效率。其實像phpunit也是支持在類方法的文檔注釋塊(docblock)中使用 @test 標注將其標記為測試方法的。這樣,徹底貫徹我們代碼即文檔的思想。
<?php
class Calculator
{
/**
* @assert (0, 0) == 0
* @assert (0, 1) == 1
* @assert (1, 0) == 1
* @assert (1, 1) == 2
* @assert (1, 2) == 4
*/
public function add($a, $b)
{
return $a + $b;
}
}
現實難題
我們到底要測什么?算法?一般很少。
大都是在編寫業務功能。而且大多數是基於數據庫的系統開發。這是我們實施PHP單元測試最大的難點所在。需要整合PHPUnit的DBUnit測試,也就是一開始就得學習DBUnit的知識。
在各種編程語言中,許多入門與中級的單元測試范例都暗示着這樣一種信息:很容易用簡單的測試來對應用程序的邏輯進行測試。但是對於以數據庫為中心的應用程序而言,這與現實相去甚遠。一旦開始使用諸如 Wordpress、TYPO3、或 Symfony(配合 Doctrine 或 Propel)之類的東西,就很容易在用 PHPUnit 時碰到超多問題:正是由於這些庫和數據庫之間實在耦合的太緊密了。
你大概會在日常工作面對的項目中經歷這一幕。你打算把你那或生疏或純熟的 PHPUnit 技能用到工作中去,結果被以下問題之一卡住了:
- 待測方法執行了一個相當大的 JOIN 操作,並且得到的數據用於計算某些重要的結果。
- 業務邏輯中混合執行了 SELECT、INSERT、UPDATE 和 DELETE 語句。
- 為了給待測方法建立合理的初始數據,需要在兩個以上(可能遠超過)表里設置測試數據。
DbUnit
DbUnit擴展大大簡化了為測試設置數據庫的操作,並且可以在對數據執行了一系列操作之后驗證數據庫的內容。
DbUnit所支持的供應商
DbUnit 目前支持 MySQL、PostgreSQL、Oracle 和 SQLite。通過集成 Zend Framework 或 Doctrine 2,也可以訪問其他數據庫系統,比如 IBM DB2 或者 Microsoft SQL Server。
數據庫測試的難點
為什么所有單元測試的范例都不包含數據庫交互?這里有個很好的理由:這類測試的建立和維護都很復雜。對數據庫進行測試時,需要考慮以下這些變數:
- 數據庫和表
- 向表中插入測試所需要的行
- 測試運行完畢后驗證數據庫的狀態
- 每個新測試都要清理數據庫
許多數據庫 API,比如 PDO、MySQLi 或者 OCI8,都十分繁瑣且書寫起來十分冗長,因此,手工進行這些步驟絕對是噩夢。
測試代碼應當盡可能簡短精確,這有若干原因:
-
你不希望因為生產代碼的小變更而需要對測試代碼進行數量可觀的修改。
-
你希望在哪怕好幾個月以后也能輕松地閱讀並理解測試代碼。
另外,必須認識到,對於代碼而言,本質上來說數據庫是全局輸入變量。測試套件中的兩個不同的測試可能是運行在同一個數據庫上的,並且可能把數據重用好多次。一個測試中出現的失敗很容易影響到后繼測試的結果,從而讓整個測試過程變得非常艱難。前面提到的清理步驟對於解決“數據庫是全局輸入”的問題是非常重要的。
DbUnit 以一種優雅的方式來幫助簡化數據庫測試中的所有這些問題。
PHPUnit 無法幫你解決的問題是,相對於不使用數據的測試而言,數據庫測試是非常慢的。隨着數據庫交互規模的增大,運行測試可能需要耗費可觀的時間。然而,只要保持每個測試所使用的數據量較小並且盡可能用非數據庫測試來對代碼進行測試,即使很大的測試套件也能輕松在一分鍾內跑完。
數據庫測試的四個階段
Gerard Meszaros 在他的書《xUnit 測試模式》中列出了單元測試的四個階段:
- 建立基架(fixture)
- 執行被測系統
- 驗證結果
- 拆除基架(fixture)
什么是基架(fixture)?
基架(fixture)是對開始執行某個測試時應用程序和數據庫所處初始狀態的描述。
對數據庫進行測試至少要處理建立與拆除的步驟,在其中完成清理工作,並將所需的基架數據寫入表內。然而對於數據庫擴展模塊而言,在數據庫測試中有很好的理由將這四個步驟還原成類似下面這樣的工作流程,這個流程對於每個測試都會完整執行:
- 清理數據庫
由於總是會有某個測試運行在並不確定表中是否有數據的數據庫上,PHPUnit 在所有指定表上執行 TRUNCATE 操作來把它們清空。
- 建立基架
PHPUnit 隨后將迭代所有指定的基架數據行並將其插入到對應的表里。
3–5. 運行測試、驗證結果、並拆除基架
在所有數據庫都完成重置並加載好初始狀態后,PHPUnit 才會執行實際的測試。這個部分的測試代碼完全不需要數據庫擴展模塊的參與,可以隨意測試任何想要測試的內容。
在測試中,驗證的目的可以使用一個名為 assertDataSetsEqual() 的特殊斷言來實現。當然,這完全是可選的。
一些術語
- 單元測試
- 集成測試
- 回歸測試
- 測試用例
- 斷言
- 基架Fixture