YII集成了單元測試和功能測試,借助phpunit和selenium實現。筆者在配置過程中遇到了不少麻煩,紀錄在此。
必要概念
selenium
selenium是個著名的自動化測試工具,可以調起本地的浏覽器來完成測試,所以可以用來自動化測試web項目。selenium分為服務端和客戶端,服務端使用java開發,所以需要一個jdk,服務端在啟動時,會啟動一個http服務,客戶端通過與服務端進行http通信,向服務端發起測試請求,服務端會自動調起浏覽器完成測試。測試人員負責編寫客戶端腳本,支持大部分主流的編程語言,當然實際上這是由於開源社區強大的威力,為不同的語言開發了針對selenium的接口程序而已,服務端和客戶端之間的協議筆者並沒有研究,因為這並不重要。
phpunit
phpunit是php語言的測試框架和工具,在進行單元測試的時候是使用它的框架,在進行功能測試的時候是使用它的工具。基於這個測試框架,有人在此基礎上做了selenium的php接口程序,作為phpunit的擴展存在。
YII框架如何集成
Yii在phpunit的基礎上,為測試做了一些簡單的封裝。因此,使用Yii來進行測試的時候,需要依賴上述兩者。
環境安裝
Firefox
selenium-server能夠識別的浏覽器並不多,似乎是IE和Firefox,所以在OSX上先安裝好Firefox浏覽器。安裝浏覽器跟一般的軟件安裝沒有大的區別,這裡不累述了。
JDK
由於selenium-server是使用java開發的,我們需要先安裝好JDK,百度搜索JDK下載安裝即可。不再累述。
selenium-server
首先來安裝selenium的server版本。在osx下,可以使用brew來安裝,比較方便:
$ brew install selenium-server-standalone
由於selenium-server的源在googleapis上,所以需要翻牆才能進行操作,事實上,如果不翻牆,其他步驟也比較困難。
安裝完成後的提示:
To have launchd start selenium-server-standalone at login: ln -sfv /usr/local/opt/selenium-server-standalone/*.plist ~/Library/LaunchAgents Then to load selenium-server-standalone now: launchctl load ~/Library/LaunchAgents/homebrew.mxcl.selenium-server-standalone.plist Or, if you don't want/need launchctl, you can just run: selenium-server -p 4444
這裡明確告訴我們通過如下命令來啟動服務端
$ selenium-server -p 4444
正如所見,通常selenium-server偵聽4444端口,如果希望修改端口,那麼相應的Yii處需要修改一下配置。
phpunit
彎路
個人理解,phpunit是一個工具和框架的集合,工具歸工具,框架歸框架。從官網的文檔看,phpunit的工具部分,是以phar包的形式發布的,而框架部分是通過pear管理的。那麼先來記錄一下這兩個概念。沒有興趣的可以跳過這節。
phar是一種php打包方案。也就是可以把一個php程序或者php網站打包在一起分發,甚至被作為一個功能模塊調用。因此,phpunit完全可以將工具程序打包成phar,執行phar的時候,通常需要使用php命令。
$ wget https://phar.phpunit.de/phpunit.phar $ chmod +x phpunit.phar $ sudo mv phpunit.phar /usr/local/bin/phpunit $ phpunit --version PHPUnit x.y.z by Sebastian Bergmann and contributors.
用上面的命令可以下載phpunit的可執行文件,可以看到這是個phar包
pear是php擴展庫的體系,因為早期php復用比較困難。編譯型語言由於語法比較緊湊和嚴謹,比較容易復用。而php由於靈活多變,復用起來學習成本比較高,於是pear就提出了一個編程規范和分發體系來實現php的功能復用,現在似乎pear已經被composer替代了(下面會說)。不過古老的東西既然已經走過彎路了不妨記下來。
在mac下可以這麼安裝pear:
$ wget http://pear.php.net/go-pear.phar $ sudo php -d detect_unicode=0 go-pear.phar
可以看到,go-pear也是個phar,只不過它是一個安裝pear的php腳本,使用php命令可以執行。安裝過程中會提示是否要修改php.ini文件:
WARNING! The include_path defined in the currently used php.ini does not contain the PEAR PHP directory you just specified: </usr/share/pear> If the specified directory is also not in the include_path used by your scripts, you will have problems getting any PEAR packages working. Would you like to alter php.ini </etc/php.ini>? [Y/n] : Y php.ini </etc/php.ini> include_path updated. Current include path : .: Configured directory : /usr/share/pear Currently used php.ini (guess) : /etc/php.ini Press Enter to continue: The 'pear' command is now at your service at /usr/bin/pear ** The 'pear' command is not currently in your PATH, so you need to ** use '/usr/bin/pear' until you have added ** '/usr/bin' to your PATH environment variable.
從這段提示我們可以得知:
pear的可執行程序安裝在/usr/bin/pear
pear有個工作目錄是/usr/share/pear,這個工作目錄需要添加到php.ini中,如果讓安裝程序自動添加的話,將是這樣的:
;***** Added by go-pear include_path=".:/usr/share/pear" ;*****
當我們在php使用require等包含其他文件的函數時,php其實除了搜索當前目錄,還會搜索include_path。這樣配置就表明,通過pear安裝的程序代碼將存放在工作目錄,而且php能夠找到,默認在工作目錄下會有一個System.php,所以以下代碼是可以工作的:
<?php require 'System.php'; ?>
使用composer安裝
本來,phpunit可以通過pear來安裝的,然而,時過境遷,在composer大行其道的時代,phpunit也宣布全面支持composer,並且放棄pear,原本通過pear的安裝方式果然都不行了。最後逼不得已,只能上composer(話說包管理工具真是多的十個手指不夠用了,將來有機會來個橫向比較)。
首先安裝composer,在翻牆狀態下:
$ brew update $ brew tap josegonzalez/homebrew-php $ brew tap homebrew/versions $ brew install php55-intl $ brew install josegonzalez/php/composer
這樣composer就裝好了。
在項目的根目錄下,創建一個composer.json,寫入:
{ "require-dev": { "phpunit/phpunit": "4.7.*", "phpunit/php-invoker": "*", "phpunit/dbunit": ">=1.2", "phpunit/phpunit-selenium": ">=1.2", "phpunit/phpunit-story": "*" } }
上面的phpunit-selenium就是基於phpunit寫的selenium客戶端庫,詳見文後的參考資料。
然後在項目根目錄下,執行
$ sudo composer install
composer會根據這個composer.json文件在根目錄創建一個vendor目錄,並將依賴的東西全部下載到這個目錄中,其中vendor/bin下面有phpunit的可執行文件。
由於是Yii的項目,所以cd到/protected/tests目錄下,執行如下命令即可啟動默認的SiteTest.php裡面的測試方法: (注意在執行前,保持selenium-server開啟狀態)
$ ../../vendor/bin/phpunit functional/SiteTest.php
會看到firefox會在執行過程中自動啟動,並由如下日志輸出:
PHPUnit 4.7.7 by Sebastian Bergmann and contributors. Warning: Deprecated configuration setting "selenium" used . Time: 11.52 seconds, Memory: 6.50Mb OK (1 test, 1 assertion)
phpunit工具程序會自動找到tests/phpunit.xml這個配置文件並根據此來進行某些配置,而Yii會利用phpunit和phpunit-selenium的框架來與selenium-server端通信,server端會啟動浏覽器,並將日志和結果等返回給客戶端。整個過程大致就是這樣的。
測試
測試是軟件開發中必不可少的環節.無論我們是否意識到,在開發Web應用的時候,我們始終都是在測試的.例如, 當我們用PHP寫了一個類時, 我們可能會用到一些注入 echo 或者 die 語句來顯示我們是否正確地實現了某個方法;當我們實現了包含一套復雜的HTML表單的web頁面時, 我們可能會試著輸入一些測試數據來確認頁面是否是按照我們的預期來交互的.更高級的開發者則會寫一些代碼來自動完成這個測試過程, 這樣一來每當我們需要測試一些東西的時候, 我們只需要調用代碼, 剩下來的就交給計算機了. 這就是所謂的 自動測試, 也是本章的主要話題.
Yii 提供的測試支持包括 單元測試 和 功能測試.
單元測試檢驗了代碼的一個獨立單元是否按照預期工作. 在面向對象編程中, 最基本的代碼單元就是類. 因此, 單元測試的主要職責就是校驗這個類所實現的每個方法工作都是正常的. 單元測試通常是由開發了這個類的人來編寫.
功能測試檢驗了特性是否按照預期工作(如:在一個博客系統裡的提交操作).與單元測試相比, 功能測試通常要高級一些, 因為待測試的特性常常牽涉到多個類. 功能測試通常是由非常了解系統需求的人編寫.(這個人既可以是開發者也可以是質量工程師).
測試驅動開發
以下展示的便是所謂的 測試驅動開發 (TDD) 的開發周期:
構建測試環境
Yii 提供的測試支持需要 PHPUnit 3.5+ 和 Selenium Remote Control 1.0+.請參照他們提供的文檔來安裝 PHPUnit 和 Selenium Remote Control.
當我們使用 yiic webapp 控制台命令來創建一個新的 Yii 應用時, 它將會生成以下文件和目錄供我們來編寫和完成測試.
testdrive/
protected/ 包含了受保護的應用文件
tests/ 包含了應用測試
fixtures/ 包含了數據 fixtures
functional/ 包含了功能測試
unit/ 包含了單元測試
report/ 包含了 coverage 報告
bootstrap.php 這個腳本在一開始執行
phpunit.xml PHPUnit 配置文件
WebTestCase.php 基於 Web 的功能測試基類
如上所示的, 我們的測試代碼主要放在 fixtures, functional 和 unit 這三個目錄下, report 目錄則用於存儲生成的代碼 coverage 報告.
我們可以在控制台窗口執行以下命令來執行測試(無論是單元測試還是功能測試):
% cd testdrive/protected/tests % phpunit functional/PostTest.php // 執行單個測試 % phpunit --verbose functional // 執行 'functional' 下的所有測試 % phpunit --coverage-html ./report unit
上面的最後一條命令將執行 unit 目錄下的所有測試然後在 report 目錄下生成出一份 code-coverage 報告. 注意要生成 code-coverage 報告必須安裝並開啟PHP的 xdebug 擴展 .
測試的引導腳本
讓我們來看看 bootstrap.php 文件裡會有些什麼. 首先這個文件有點特殊,因為它看起來很像是 入口腳本, 而它也正是我們執行一系列測試的入口.
$yiit='path/to/yii/framework/yiit.php'; $config=dirname(__FILE__).'/../config/test.php'; require_once($yiit); require_once(dirname(__FILE__).'/WebTestCase.php'); Yii::createWebApplication($config);
如上所示, 首先我們包含了來自 Yii 框架的 yiit.php 文件, 它初始化了一些全局常量以及必要的測試基類.然後我們使用 test.php 這個配置文件來創建一個應用實例.如果你查看 test.php 文件, 你會發現它是繼承自 main.php 這個配置文件的, 只不過它多加了一個類名為 [CDbFixtureManager] 的 fixture 應用組件.
return CMap::mergeArray( require(dirname(__FILE__).'/main.php'), array( 'components'=>array( 'fixture'=>array( 'class'=>'system.test.CDbFixtureManager', ), /* 去除以下注釋可為測試提供一個數據庫連接. 'db'=>array( 'connectionString'=>'DSN for test database', ), */ ), ) );當我執行那些涉及到數據庫操作的測試時, 我們應該提供一個測試專用的數據庫以便測試執行不會干擾到正常的開發或者生產活動. 這樣一來, 我們紙需要去除上面 db 配置的注釋, 然後填寫 connectionString 屬性的用以連接到數據庫的DSN(數據源名稱)即可.
通過這樣一個啟動腳本, 當我們執行單元測試時, 我們便可以獲得一個與服務需求類似的應用實例, 而主要的不同就是測試擁有一個 fixture 管理器以及它專屬的測試數據庫.
定義特定狀態(Fixtures)
自動測試需要被執行很多次.為了確保測試過程是可以重復的, 我們很想要在一些可知的狀態下進行測試, 這個狀態我們稱之為 特定狀態. 舉個例子,在一個博客應用中測試文章創建特性, 每次當我們進行測試時, 與文章相關的表(例如. Post 表 , Comment 表)應該被恢復到一個特定的狀態下. PHPUnit 文檔 已經很好的描述了一般的特定狀態的構建. 而本節主要介紹怎樣像剛才描述的例子那樣構建數據庫特定狀態.
設置構建數據庫的特定狀態,這恐怕是測試以數據庫為後端支持的應用最耗時的部分之一.Yii 引進的 [CBbFixtureManager] 應用組件可以有效的減輕這一問題.當進行一組測試的時候,它基本上會做以下這些事情:
在所有測試運行之前,它重置測試相關數據為可知的狀態.
在單個測試運行之前, 它將特定的表重置為可知狀態.
在一個測試方法執行過程中, 它提供了供給特定狀態的行數據的訪問接口.
請按如下使用我們在 應用配置 中配置的 [CDbFixtureManager].
return array( 'components'=>array( 'fixture'=>array( 'class'=>'system.test.CDbFixtureManager', ), ), );
然後我們在目錄 protected/tests/fixtures下提供一個特定狀態數據. 這個目錄可以通過配置應用配置文件中的 [CDbFixtureManager::basePath] 屬性指定為其他目錄.特定狀態數據是由多個稱之為特定狀態文件的PHP文件組合而成.每個特定狀態文件返回一個數組, 代表數據的一個特定表的初始行.文件名和表名相同.以下則是將 Post 表的特定狀態數據存儲於名為 Post.php 文件裡的例子.
<?php return array( 'sample1'=>array( 'title'=>'test post 1', 'content'=>'test post content 1', 'createTime'=>1230952187, 'authorId'=>1, ), 'sample2'=>array( 'title'=>'test post 2', 'content'=>'test post content 2', 'createTime'=>1230952287, 'authorId'=>1, ), );
正如我們所見, 上面返回了兩行數據. 每一行都表示一個數組,其鍵是表的字段名,其值則是對應的字段值.每行的索引都是稱之為行別名的字符串(例如: simple1, simple2). 稍後當我們編寫測試腳本的時候, 我們可以方便地通過它的別名調用這行數據.你也許注意到了我們並未在上述特定狀態中指定 id 字段的值. 這是因為 id 字段已經被定義為自增主鍵了,它的值也會在我們插入新數據的時候自動生成.
當 [CDbFixtureManager] 第一次被引用時, 它會仔細檢查所有的特定狀態文件然後使用他們重置對應的表.它通過清空表,重置表主鍵的自增序列值,然後插入來自特定狀態文件的數據行到表中來重置表.
有時候,我們可能不想在一套測試前重置特定狀態文件裡描述的每一個表, 因為重置太多的特定狀態文件可能需要很多時間.這種情況下,我們可以寫一個PHP腳本來定制這個初始化過程.這個腳本應該被保存在存放特定狀態文件的目錄下,並命名為 init.php.當 [CDbFixtureManager] 檢測到了這個腳本的存在, 它將執行這個腳本而不是重置每一個表.
不喜歡使用默認方式來重置表也是可以的,例如: 清空表然後插入特定狀態數據. 如果是這種情況, 我們可以為指定的特定狀態文件編寫一個初始化腳本.這個腳本必須名稱為表名+.init.php. 例如: Post 表的初始化腳本文件就是 Post.init.php. 當 [CDbFixtureManager] 發現了這個腳本,它將執行這個腳本而不是采用默認的方式去重置該表.
Tip: 太多的特定狀態文件大大延長了測試時間.因此, 你應該只為那些在測試中數據會發生變化的表提供特定狀態文件. 那些做為查找服務的表不會改變,因此不需要特定狀態文件.