自 Zend Framework 1.6 之後就提供了 Zend_Test 這個套件,協助我們為 Zend Framework 的 MVC 做單元測試。然而當時也許是因為大家對 Zend Framework 的新架構還不熟悉,加上 Zend_Test 套件有許多小地方還不是很完善,因此就很少有人利用它來做測試。
而到了 Zend Framework 1.8 版之後, Zend_Test 便修正了許多缺點;而且也因為 Zend_Application 套件的引入,讓 Zend_Test 在 Controller 上的測試更加方便,也使得網路上許多高手開始為它編寫教學。
不過官方的文件並沒有特別告訴我們怎麼去建構 Zend Framework Testing 的環境,所以以下我以 Zend Framework 1.10 再加上 PHPUnit 3.4 為例,帶領大家進入 Zend Framework Testing 的世界。
準備 Zend Framework 專案
更新:這邊的步驟我獨立成一篇新文章了,請參考「建立 Portable 的 ZF 專案」;但請將專案名稱改為 unittest ,其他步驟則相同。
在 NetBeans 中加入專案
NetBeans 近來對 PHP 的支援變得非常強大,讓我一用就愛上它。到 6.8 版為止, NetBeans 還沒有正式支援 Zend Framework ,不過這並不影響我們用它來開發以 Zend Framework 為架構的專案。
而這裡會提到 NetBeans ,主要是因為它支援了 PHPUnit !而且還有圖形化介面;這使得我們在對 PHP 專案進行單元測試時更加便利!
回到剛剛我們建立的 Zend Framework 專案,我們可以在 NetBeans 裡加入一個現成的 PHP 專案。在工具列中,選擇 File > New Project… 後,會出現以下視窗:
然後要選擇現存專案的位置:
最後設定專案執行時的設置:
這樣一來我們就可以在 NetBeans 裡開發 Zend Framework 的專案了。
Zend_Test 基礎
Zend_Test 主要是基於 PHPUnit 這個 PHP 界公認最完善的自動化測試套件所開發的,所以不論是在設置環境或是在撰寫測試上,其實都是以 PHPUnit 的架構去完成。
為了避免本文內容過於冗長,這裡我已經先行設定好 PHPUnit 的執行環境;因此在繼續以下的步驟前,請大家先確認自己是否可以正確執行 PHPUnit 。
在 Zend Framework 為我們們準備好的專案目錄架構中,已經事先建立了 tests 這個供我們放置測試的目錄,所以現在我們可以先試試看用 PHPUnit 來執行我們的測試。假設我們目前的工作路徑是 D:\WEB\zf\unittest\ ,我們要先切換到 tests 目錄下:
1
| |
然後用 PHPUnit 提供的 Command Line Interface (CLI) 工具 phpunit 來執行測試:
1
| |
我們會發現執行的結果出現了以下錯誤:
1
| |
原來, phpunit 這個工具在我們對一個目錄進行測試時,會自動去剖析該目錄下的 phpunit.xml 內容;而 phpunit.xml 在 Zend_Tool 產生之後,並沒有任何內容,所以就使得 PHPUnit 丟出了一個異常。
所以這裡我們先簡單將它變成一個正確的 XML 檔案,在 D:\WEB\zf\unittest\tests\phpunit.xml 中我們加入:
1
| |
然後在 Command Line 環境下再跑一次測試,結果如下:
這表示 PHPUnit 認得我們的測試環境啦!
建立測試
其實上面也只是讓 phpunit 能在我們的專案上啟用而已,但我們的測試主角根本還沒出現,所以接下來我們要開始來建立測試了。
Zend_Tool 建立出來的專案架構,其實有個預設的前置名稱,叫做 Application ;這個 Application 之後都做為專案中 Model 的前置名稱。一般來說,我都會將它改為像是 MyApp 或是 Shop 等等諸如此類符合目前專案性質的名稱。 Zend_Tool 也提供我們修改這個前置名稱的機制:
1
| |
執行結果如下:
1 2 3 4 | |
要注意的是,如果在修改前就已經存在的 Model ,它們的前置名稱就要手動改成新的。
建立第一個 Model
接著我們建立第一個 Model Class :
1
| |
執行結果如下:
1 2 | |
在 D:\WEB\zf\unittest/application/models/MyClass1.php 中就是 MyClass1 的內容:
1 2 3 4 | |
當然這個檔案目前只有簡單的類別定義而已,其他就是要靠我們自己手動完成。不過由於我們的重點是測試,所以這裡我們就不管 MyClass1 的內容。
用 NetBeans 產生 Test Case
前面提到 NetBeans 支援 PHPUnit ,因此這裡我們就直接試著用它來幫我們產生測試檔案。切換到專案目錄的「 Source Files/application/models 」下,在 MyClass1.php 上按下右鍵選擇「工具 / Create PHPUnit tests 」,我們就可以建立針對 MyClass1 類別的 Test Case :
但因為 NetBeans 還不知道測試目錄的所在位置,因此它會提示我們輸入測試目錄;在這裡,我們當然就是選擇 D:\WEB\zf\unittest\tests :
在按下確定之後, NetBeans 就會自動打開產生好的 Test Case 檔案。在本例中,它的位置就是在 D:\WEB\zf\unittest\tests\application\models\MyApp_Model_MyClass1Test.php 。
另外 NetBeans 也會把 tests 從原來的 Source Files 中獨立成 Test Files 目錄結構,但別忘了它實際的位置還是在 D:\WEB\zf\unittest\ 下。
目錄結構對應
繼續下個步驟之前,我們先來整個專案的目錄結構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
從上面的架構圖可以看到,在這個 tests 目錄下的 application 及 library ,就是對應著我們的專案目錄下的 application 及 library 。換句話說,我們在專案目錄下的 application/models 所建立的 Model Class ,它們的測試程式就可以放在 tests/application/models 下,其他以此類推。
執行測試類別
在我們建立好 MyApp_Model_MyClass1Test 這個測試類別後,就可以在上面撰寫測試案例了。這裡我們先簡單加入一個空的測試案例,即 testMethod1 方法:
1 2 3 4 5 6 7 8 9 | |
接著就可以執行測試了。
這裡雖然我們可以用 phpunit 來執行測試,不過 NetBeans 也可以幫我們代勞。我們可以在 NetBeans 的 Project 視窗中,對 unittest 專案名稱按下滑鼠右鍵,執行 Test 指令 (或直接按下 Alt + F6 ) ,這樣就會開始執行測試了。
完成測試後,就會出現下圖中的 Test Results 視窗。
這看起來是不是比先前的 Command Line 輸出結果好多了呢?
測試環境的自動載入
在測試時,其實我希望要測試用的類別能被自動載入,而不需要加入一堆的 require_once 。以往我在舊專案的做法是在每個測試案例之前,引用一個 bootstrap.php 檔,內容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
而在新架構中, Zend_Tool 很貼心地為我們產生好了這個檔案,也就是 D:\WEB\zf\unittest\tests\application\bootstrap.php 。
註:不過 D:\WEB\zf\unittest\tests\application\bootstrap.php 裡面的內容是空的,我們得自己把它加入。
但是每次都要在測試類別檔載入這個 bootstraop.php 也是相當麻煩,還好 PHPUnit 讓我們可以在 phpunit.xml 中去加入對 bootstraop.php 的自動引用。接下來請將 D:\WEB\zf\unittest\tests\phpunit.xml 的內容改為:
1
| |
這麼一來,當我們執行 phpunit 時,就會自動引入 bootstraop.php 了。而這裡要注意的是, ./application/bootstrap.php 這個路徑是相對於 phpunit.xml 的,其他非絕對路徑的設定也是。
不過上面的 bootstraop.php 寫法在新的 Zend Framework 架構中就不適用了,因為它的自動載入只能應付類別名稱為 Xxx_Yyy_Zzz 並對應到檔名為 Xxx/Yyy/Zzz.php 的模式;而新架構中的 Model 則是位於 application/models 之下,也就是說我們的 MyApp_Model_MyClass1 這個類別的實際位置是 D:\WEB\zf\unittest\application\models\MyApp_Model_MyClass1.php ,因此我們得想辦法讓 bootstrap.php 能自動載入新架構的 Model Class 。
還好 Zend_Loader 提供了 Zend_Loader_Autoloader_Resource 這個套件,來幫我們載入 Zend Framework MVC 架構下的 Model Class 。所以我們的 bootstrap.php 就可以改成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
註: Zend_Loader_Autoloader_Resource 的詳細用法請參考官方手冊 Resource Autoloaders 一節的說明。
現在我們可以試著把 MyApp_Model_MyClass1Test.php 中的 require_once 敘述移除,試試看 phpunit 會不會自動載入。
不過特別要注意的是, bootstrap.php 只會在 phpunit.xml 被載入時自動被引用,因此我們就一定要透過 phpunit . 這樣的方式來執行測試,不然就會發生找不到 Model Class 檔案的狀況。如果我們只想單獨執行某個測試類別時 (例如 MyApp_Model_MyClass1Test ) ,就必須改用以下的方式執行: (假設當前工作目錄是在 D:\WEB\zf\unittest\tests )
1
| |
而在 NetBeans 中,我們可以在 Project 視窗的 Test Files 上按下滑鼠右鍵選擇 Properties ,就會出現以下視窗:
然後我們再指定 phpunit.xml 的位置,這樣就算只對單一的 Model 進行測試, NetBeans 也會自動幫我們載入 phpunit.xml 了。
加入更多測試
一個專案裡要測試的 Model 當然不可能只有一個,接著我們再加入新的 Model 。在 D:\WEB\zf\unittest 之下,我們執行:
1
| |
然後再透過 NetBeans 的 Create PHPUnit tests ,幫它建立一個測試類別 MyApp_Model_MyClass2Test 。當然也別忘了幫這個測試類別加入測試案例,以免執行測試時會出現沒有測試案例的警告。
不是只有 Model 可以被測試,在 Zend_Test 裡也提供了測試 Controller 的方法;方法很簡單,以預設的 IndexController 為例,我們一樣也是在 NetBeans 的 Project 視窗中,對 unittest/application/controllers/IndexController.php 執行 Create PHPUnit tests 就可以了。
但是這樣卻有可能出現以下的錯誤訊息,而無法成功建立 IndexController 的測試類別。
而查看輸出視窗 (Ctrl + 4) ,實際的錯誤訊息是:
1
| |
這是因為 NetBeans 其實是透過 phpunit –skeleton-test 這個指令來幫助我們建立測試類別,而 –skeleton-test 會利用 PHP 5 的 Reflection 機制幫我們找出類別裡的方法;但 Reflection 也會需要相關引用到的類別的定義 (這裡就是 Zend_Controller_Action ) ,因此在沒有事先定義好的狀況下, phpunit 就會回報錯誤了。
解決的方法很簡單,我們只要在 IndexController 類別的上方加入:
1
| |
就可以再執行 Create PHPUnit tests 指令了。
完成後, NetBeans 就會產生 D:\WEB\zf\unittest\tests\application\controllers\IndexControllerTest.php 這個檔案了。
測試 Controller
雖然前面我們產生了 IndexControllerTest 這個測試類別,但是它卻不能用來測試 IndexController 的流程。這是因為我們沒有去啟動 Zend_Application 及相關的 Resource ,所以這裡我們要借重 Zend_Test 提供的 Zend_Test_PHPUnit_ControllerTestCase 類別來幫我們進行測試。請將 D:\WEB\zf\unittest\tests\application\controllers\IndexControllerTest.php 的內容改為:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
在這裡我們要借重 Zend_Test_PHPUnit_ControllerTestCase 提供的 bootstrap 機制,幫我們處理有關應用程式設定與 Front Controller 等相關資源的啟動;然後我們就可以簡單地透過 Zend_Test_PHPUnit_ControllerTestCase 提供的方法 (例如 dispatch ) ,來幫我們測試 Controller 的流程。
上面的例子裡我們只很簡單地測試首頁,至於更進階的 Controller 測試,有機會我會再分享給大家。
進階的 phpunit.xml 設定
在 phpunit.xml 中可以設定的項目很多,可以參考官方手冊的說明,這邊我們簡單地探討幾個常用的項目。
testsuite
testsuite 標籤主要是幫我們透過 PHPUnit_Framework_TestSuite 去組織多個測試類別,例如以下範例:
1 2 3 4 5 | |
首先 testsuite 的 name 屬性是讓我們設定 Suite 的名稱,而 directory 標籤則是告訴 phpunit ,這個 Suite 包含了 D:\WEB\zf\unittest\tests 下所有檔名結尾為 Test.php 的測試檔案。
另外還要注意一點,在還沒加入 testsuite 之前,我們要在 D:\WEB\zf\unittest\tests 用 "phpunit ." 來指定目錄執行測試;而加入 testsuite 標籤後,我們就可以直接在 D:\WEB\zf\unittest\tests 執行 phpunit ,而不用再指定測試的路徑了。
filter
filter 標籤主要是幫我們過濾程式碼涵蓋範圍報告的項目,避免出現過多不相干類別的報告內容。 filter 分為白名單與黑名單兩種,以下我們以白名單為主:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
這裡主要是告訴 phpunit 只處理 D:\WEB\zf\unittest\application 下的類別 (注意!不是 tests\application !) ,但排除掉 D:\WEB\zf\unittest\application\Bootstrap.php 、 D:\WEB\zf\unittest\application\controllers\ErrorController.php 以及 D:\WEB\zf\unittest\application\ 下所有的 .phtml 樣板檔。
不過 filter 通常會搭配稍後要說明的 logging 標籤一起使用。
logging
logging 標籤主要用來產生測試報告,它支援相當多的格式,以下我們以 HTML 格式為主:
1 2 3 4 5 6 7 8 | |
在執行測試前,別忘了在 D:\WEB\zf\unittest\tests 下建立一個 log 目錄,避免 phpunit 回報錯誤。執行完測試後,就會在 D:\WEB\zf\unittest\tests\log 產生一個 testdox.html 檔案及一個 report 資料夾。
textdox.html 會以我們在測試類別裡寫的測試案例為名稱來做為驗證項目名稱,例如 testDoSomething 就會變成 Do something 。而用瀏覽器開啟 report/index.html 後,我們就能看到完整的程式碼覆蓋率報告。
小結
最後我們再整理一下重點:
-
我們可以在專案中建立一個 tests 目錄,供我們放置測試類別。
-
PHPUnit 可以透過 phpunit.xml 及 bootstrap.php 來做自動化的測試環境。
-
phpunit.xml 主要是用來設定 phpunit 執行時的選項。
-
bootstrap.php 主要的工作是設定測試時會用到的環境常數、引入路徑以及類別自動載入機制等。
-
NetBeans 對 PHPUnit 有強大的支援,不論在建立測試及執行測試上,都非常直覺。
-
即便不使用 NetBeans ,我們還是可以透過 phpunit 這個 CLI 工具來完成自動化測試。
雖然這裡我是以 Zend Framework 專案為主,但大家還是可以仿照這樣的機制,在自己的專案裡去建構自動化測試。
還猶豫什麼?快去試試吧!









