Zend Framework Testing 入門 - 測試環境建置篇

分類: 技術分享 作者: jaceju

21 五月 2010

本文轉自:Zend Framework Testing 入門 - 測試環境建置篇

自 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 專案

要進行測試前,我們得先要有個 Zend Framework 專案。假設這個專案名稱叫 unittest ,原始檔位於 D:\WEB\zf\unittest 上,我們用 Zend Framework 提供的 Zend_Tool工具來建立它:

zf create project unittest

然後我們切換到該專案目錄下:

cd unittest

註:雖然我是以 Win32 為主要環境,但 Linux 上的操作步驟也是雷同。

由於我個人偏好搬動整個目錄就可以移植專案的模式,因此我就不採用 Zend Framework 原本將 index.php 放在 public 目錄下的架構;以下動作的目的就是把 index.php 從 public 目錄移出來。

首先先把 D:\WEB\zf\unittest\public\index.php 和 D:\WEB\zf\unittest\public\.htaccess 複製到 D:\WEB\zf\unittest 下,然後修改 D:\WEB\zf\unittest\.zfproject.xml ,把上方紅框處的 index.php 和 .htaccess 的 XML tag 複製一份到下方的紅框處:

修改 .zfproject.xml

接下來要修改 D:\WEB\zf\unittest\index.php 中 application 目錄的相對位置;找到:

|| define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));

改為:

|| define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/application'));

然後我們要在 D:\WEB\zf\unittest\.htaccess 加入當前環境的常數,也就是在它的最上面一行加入:

SetEnv APPLICATION_ENV development

原來的 D:\WEB\zf\unittest\public\index.php 則用來阻擋使用者瀏覽 public 目錄,所以我們把它改成:

<?php
header('HTTP/1.1 403 Forbidden');

而原來的 D:\WEB\zf\unittest\public\.htaccess 則是將 Rewrite Engine 關閉,讓外界可以直接存取:

RewriteEngine Off

這樣就準備好我們的專案了。

在 NetBeans 中加入專案

NetBeans 近來對 PHP 的支援變得非常強大,讓我一用就愛上它。到 6.8 版為止, NetBeans 還沒有正式支援 Zend Framework ,不過這並不影響我們用它來開發以 Zend Framework 為架構的專案。

而這裡會提到 NetBeans ,主要是因為它支援了 PHPUnit !而且還有圖形化介面;這使得我們在對 PHP 專案進行單元測試時更加便利!

回到剛剛我們建立的 Zend Framework 專案,我們可以在 NetBeans 裡加入一個現成的 PHP 專案。在工具列中,選擇 File > New Project... 後,會出現以下視窗:

建立 NetBeans 專案

然後要選擇現存專案的位置:

選擇專案位置

最後設定專案執行時的設置:

完成專案其它設定

這樣一來我們就可以在 NetBeans 裡開發 Zend Framework 的專案了。

Zend_Test 基礎

Zend_Test 主要是基於 PHPUnit 這個 PHP 界公認最完善的自動化測試套件所開發的,所以不論是在設置環境或是在撰寫測試上,其實都是以 PHPUnit 的架構去完成。

為了避免本文內容過於冗長,這裡我已經先行設定好 PHPUnit 的執行環境;因此在繼續以下的步驟前,請大家先確認自己是否可以正確執行 PHPUnit 。

在 Zend Framework 為我們們準備好的專案目錄架構中,已經事先建立了 tests 這個供我們放置測試的目錄,所以現在我們可以先試試看用 PHPUnit 來執行我們的測試。假設我們目前的工作路徑是 D:\WEB\zf\unittest\ ,我們要先切換到 tests 目錄下:

cd tests

然後用 PHPUnit 提供的 Command Line Interface (CLI) 工具 phpunit 來執行測試:

phpunit .

我們會發現執行的結果出現了以下錯誤:

PHPUnit_Framework_Exception: Could not load "D:\WEB\zf\unittest\tests\phpunit.xml".

原來, phpunit 這個工具在我們對一個目錄進行測試時,會自動去剖析該目錄下的 phpunit.xml 內容;而 phpunit.xml 在 Zend_Tool 產生之後,並沒有任何內容,所以就使得 PHPUnit 丟出了一個異常。

所以這裡我們先簡單將它變成一個正確的 XML 檔案,在 D:\WEB\zf\unittest\tests\phpunit.xml 中我們加入:

<phpunit></phpunit>

然後在 Command Line 環境下再跑一次測試,結果如下:

執行 phpunit

這表示 PHPUnit 認得我們的測試環境啦!

建立測試

其實上面也只是讓 phpunit 能在我們的專案上啟用而已,但我們的測試主角根本還沒出現,所以接下來我們要開始來建立測試了。

Zend_Tool 建立出來的專案架構,其實有個預設的前置名稱,叫做 Application ;這個 Application 之後都做為專案中 Model 的前置名稱。一般來說,我都會將它改為像是 MyApp 或是 Shop 等等諸如此類符合目前專案性質的名稱。 Zend_Tool 也提供我們修改這個前置名稱的機制:

zf change application.class-name-prefix MyApp

執行結果如下:

Note: the name provided "MyApp" was altered to "MyApp_" for correctness.
Note: All existing models will need to be altered to this new namespace by hand
application.ini updated with new appnamespace MyApp_
Updating project profile 'D:\WEB\zf\unittest/.zfproject.xml'

要注意的是,如果在修改前就已經存在的 Model ,它們的前置名稱就要手動改成新的。

建立第一個 Model

接著我們建立第一個 Model Class :

zf create model MyClass1

執行結果如下:

Creating a model at D:\WEB\zf\unittest/application/models/MyClass1.php
Updating project profile 'D:\WEB\zf\unittest/.zfproject.xml'

在 D:\WEB\zf\unittest/application/models/MyClass1.php 中就是 MyClass1 的內容:

<?php
 
class MyApp_Model_MyClass1
{
 
 
}

當然這個檔案目前只有簡單的類別定義而已,其他就是要靠我們自己手動完成。不過由於我們的重點是測試,所以這裡我們就不管 MyClass1 的內容。

用 NetBeans 產生 Test Case

前面提到 NetBeans 支援 PHPUnit ,因此這裡我們就直接試著用它來幫我們產生測試檔案。切換到專案目錄的「 Source Files/application/models 」下,在 MyClass1.php 上按下右鍵選擇「工具 / Create PHPUnit tests 」,我們就可以建立針對 MyClass1 類別的 Test Case :

產生 Model 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\ 下。

目錄結構對應

繼續下個步驟之前,我們先來整個專案的目錄結構:

unittest
 |--application
 |   |--models
 |       |--MyClass1.php
 |--library
 |--docs
 |--public
 |--tests
 |   |--application
 |       |--models
 |           |--MyApp_Model_MyClass1Test.php
 |   |--library
 |   |--phpunit.xml
 |--.zfproject.xml
 |--.htaccess
 |--index.php

從上面的架構圖可以看到,在這個 tests 目錄下的 application 及 library ,就是對應著我們的專案目錄下的 application 及 library 。換句話說,我們在專案目錄下的 application/models 所建立的 Model Class ,它們的測試程式就可以放在 tests/application/models 下,其他以此類推。

執行測試類別

在我們建立好 MyApp_Model_MyClass1Test 這個測試類別後,就可以在上面撰寫測試案例了。這裡我們先簡單加入一個空的測試案例,即 testMethod1 方法:

// ... 略 ...
class MyApp_Model_MyClass1Test extends PHPUnit_Framework_TestCase
{
    // ... 略 ...
 
    public function testMethod1()
    {
 
    }
}

接著就可以執行測試了。

這裡雖然我們可以用 phpunit 來執行測試,不過 NetBeans 也可以幫我們代勞。我們可以在 NetBeans 的 Project 視窗中,對 unittest 專案名稱按下滑鼠右鍵,執行 Test 指令 (或直接按下 Alt + F6 ) ,這樣就會開始執行測試了。

執行專案測試

完成測試後,就會出現下圖中的 Test Results 視窗。

完成測試的畫面

這看起來是不是比先前的 Command Line 輸出結果好多了呢?

測試環境的自動載入

在測試時,其實我希望要測試用的類別能被自動載入,而不需要加入一堆的 require_once 。以往我在舊專案的做法是在每個測試案例之前,引用一個 bootstrap.php 檔,內容如下:

<?php
define('APP_PATH', realpath(dirname(__FILE__) . '/../app/'));
 
set_include_path(implode(PATH_SEPARATOR, array(
    realpath(APP_PATH . '/lib'),
    realpath(APP_PATH . '/base'),
    realpath(APP_PATH . '/mod/common/models'),
    get_include_path(),
)));
 
function autoload($className)
{
    $className = str_replace('_', '/', $className);
    require_once "$className.php";
}
 
spl_autoload_register('autoload');

而在新架構中, 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 的內容改為:

<phpunit bootstrap="./application/bootstrap.php" />

這麼一來,當我們執行 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 就可以改成:

<?php
error_reporting(E_ALL | E_STRICT);
 
date_default_timezone_set('Asia/Taipei');
 
define('APPLICATION_ENV', 'testing');
define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../../application'));
define('APPLICATION_CONFIG', APPLICATION_PATH . '/configs/application.ini');
 
set_include_path(implode(PATH_SEPARATOR, array(
    realpath(APPLICATION_PATH . '/../library'),
    realpath(APPLICATION_PATH . '/../tests'),
    get_include_path(),
)));
 
require_once 'Zend/Loader/Autoloader.php';
$autoloader = Zend_Loader_Autoloader::getInstance();
$resourceLoader = new Zend_Loader_Autoloader_Resource(array(
    'basePath'      => APPLICATION_PATH,
    'namespace'     => 'MyApp',
    'resourceTypes' => array(
        'model' => array(
            'path' => 'models/',
            'namespace' => 'Model',
        ),
    ),
));
$autoloader->pushAutoloader($resourceLoader);

註: 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 )

phpunit application\models\MyApp_Model_MyClass1Test.php --configuration ./phpunit.xml

而在 NetBeans 中,我們可以在 Project 視窗的 Test Files 上按下滑鼠右鍵選擇 Properties ,就會出現以下視窗:

NetBeans 中的 PHPUnit 設定

然後我們再指定 phpunit.xml 的位置,這樣就算只對單一的 Model 進行測試, NetBeans 也會自動幫我們載入 phpunit.xml 了。

加入更多測試

一個專案裡要測試的 Model 當然不可能只有一個,接著我們再加入新的 Model 。在 D:\WEB\zf\unittest 之下,我們執行:

zf create model MyClass2

然後再透過 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) ,實際的錯誤訊息是:

Fatal error: Class 'Zend_Controller_Action' not found in D:\WEB\zf\unittest\application\controllers\IndexController.php on line 3

這是因為 NetBeans 其實是透過 phpunit --skeleton-test 這個指令來幫助我們建立測試類別,而 --skeleton-test 會利用 PHP 5 的 Reflection 機制幫我們找出類別裡的方法;但 Reflection 也會需要相關引用到的類別的定義 (這裡就是 Zend_Controller_Action ) ,因此在沒有事先定義好的狀況下, phpunit 就會回報錯誤了。

解決的方法很簡單,我們只要在 IndexController 類別的上方加入:

require_once 'Zend/Controller/Action.php';

就可以再執行 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 的內容改為:

<?php
class IndexControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
    protected function setUp()
    {
        $this->bootstrap = array($this, 'appBootstrap');
        parent::setUp();
    }
 
	public function appBootstrap()
	{
	  	$this->application = new Zend_Application(APPLICATION_ENV, APPLICATION_CONFIG);
	  	$this->application->bootstrap();
	}
 
    public function testHome()
    {
        $this->dispatch('/');
        $this->assertAction("index");
        $this->assertController("index");
        $this->assertResponseCode(200);
    }
}

在這裡我們要借重 Zend_Test_PHPUnit_ControllerTestCase 提供的 bootstrap 機制,幫我們處理有關應用程式設定與 Front Controller 等相關資源的啟動;然後我們就可以簡單地透過 Zend_Test_PHPUnit_ControllerTestCase 提供的方法 (例如 dispatch ) ,來幫我們測試 Controller 的流程。

上面的例子裡我們只很簡單地測試首頁,至於更進階的 Controller 測試,有機會我會再分享給大家。

進階的 phpunit.xml 設定

在 phpunit.xml 中可以設定的項目很多,可以參考官方手冊的說明,這邊我們簡單地探討幾個常用的項目。

testsuite

testsuite 標籤主要是幫我們透過 PHPUnit_Framework_TestSuite 去組織多個測試類別,例如以下範例:

<phpunit bootstrap="./application/bootstrap.php">
    <testsuite name="My App">
        <directory>./</directory>
    </testsuite>
</phpunit>

首先 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 分為白名單與黑名單兩種,以下我們以白名單為主:

<phpunit bootstrap="./application/bootstrap.php">
    <!---->
    <filter>
        <whitelist>
            <directory suffix=".php">../application/</directory>
            <exclude>
            	<file>../application/Bootstrap.php</file>
            	<file>../application/controllers/ErrorController.php</file>
               <directory suffix=".phtml">../application/</directory>
            </exclude>
        </whitelist>
    </filter>
</phpunit>

這裡主要是告訴 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 格式為主:

<phpunit bootstrap="./application/bootstrap.php">
    <!---->
    <logging>
        <log type="coverage-html" target="./log/report" charset="UTF-8"
            yui="true" highlight="true" lowUpperBound="50" highLowerBound="80"/>
        <log type="testdox-html" target="./log/testdox.html" />
    </logging>
</phpunit>

在執行測試前,別忘了在 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 專案為主,但大家還是可以仿照這樣的機制,在自己的專案裡去建構自動化測試。

還猶豫什麼?快去試試吧!

我要留言

關於這裡

這個部落格分享了哇寶在電子商務領域的技術及資訊,希望能讓更多人一起為台灣的網路產業加油。