寫PHP程式碼你搞過單元測試嗎

x3d發表於2015-04-29

很多人會說,其實一開始我內心是想做單元測試(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)是對開始執行某個測試時應用程式和資料庫所處初始狀態的描述。

對資料庫進行測試至少要處理建立與拆除的步驟,在其中完成清理工作,並將所需的基架資料寫入表內。然而對於資料庫擴充套件模組而言,在資料庫測試中有很好的理由將這四個步驟還原成類似下面這樣的工作流程,這個流程對於每個測試都會完整執行:

  1. 清理資料庫

由於總是會有某個測試執行在並不確定表中是否有資料的資料庫上,PHPUnit 在所有指定表上執行 TRUNCATE 操作來把它們清空。

  1. 建立基架

PHPUnit 隨後將迭代所有指定的基架資料行並將其插入到對應的表裡。

3–5. 執行測試、驗證結果、並拆除基架

在所有資料庫都完成重置並載入好初始狀態後,PHPUnit 才會執行實際的測試。這個部分的測試程式碼完全不需要資料庫擴充套件模組的參與,可以隨意測試任何想要測試的內容。

在測試中,驗證的目的可以使用一個名為 assertDataSetsEqual() 的特殊斷言來實現。當然,這完全是可選的。

一些術語

  • 單元測試
  • 整合測試
  • 迴歸測試
  • 測試用例
  • 斷言
  • 基架Fixture


相關文章