如今,咱們擁有了能夠工做的全套基礎設施,讓咱們回到在設計階段時定義的第一個特性,讓咱們先爲它寫一個驗收測試。
php
端到端驗收測試的要點就是,咱們必須只經過UI來處理咱們的應用。咱們不能用任何方式去直接訪問數據庫,更糟糕的是,直接訪問應用的文件系統。因此,測試一個數據庫查詢,數據應首先插入數據庫。而後依靠UI來完成測試。git
這裏是結果測試的步驟:github
打開添加客戶數據到數據庫的界面web
添加第一個客戶到數據庫。你應該看到客戶列表,只有一條記錄正則表達式
添加第二個客戶到數據庫。你應該看到客戶列表裏有兩條數據了shell
打開經過手機號碼查詢客戶的界面數據庫
用客戶1的手機號碼進行查詢,你應該看到界面查詢結果中有客戶1,而且沒有客戶2json
所以,測試強制咱們提供三個頁面:新建客戶,客戶列表,以及查詢頁面。這部分就是爲何咱們要稱之爲」端到端測試「。數組
翻譯成Codeception的測試代碼,剛剛描述的過程就像這樣:瀏覽器
$I = new \AcceptanceTester\CRMOperatorSteps($scenario); $I->wantTo('add two different customers to database'); $I->amInAddCustomerUi(); $first_customer = $I->imageCustomer(); $I->fillCustomerDataForm($first_customer); $I->submitCustomerDataForm(); $I->seeIAmInListCustomersUi(); $I->amInAddCustomerUi(); $second_customer = $I->imagineCustomer(); $I->fillCustomerDataForm($second_customer); $I->submitCustomerDataForm(); $I->seeIAmInListCustomersUi(); $I = new \AcceptanceTester\CRMUserSteps($scenario); $I->wantTo('query the customer info using his phone number'); $I->amInQueryCustomerUi(); $I->fillInPhoneFieldWithDataForm($first_customer); $I->clickSearchButton(); $I->seeIAmInListCustomersUi(); $I->seeCustomerInList($first_customer); $I->dontSeeCustomerInList($second_customer);
讓咱們把這段代碼放到 tests/acceptance/QueryCustomerByPhoneNumberCept.php 文件中。這就是本章咱們要完成的目標。
讓咱們從新瀏覽這些不那麼顯而易見的測試腳本。
首先,咱們將整個場景拆分紅兩個邏輯部分,使用了兩個Acceptance的子類來強調它們的不一樣之處。Codeception有一個很是好的輔助生成不一樣Guy類子類的方法,使用它,咱們能夠用下面的命令來建立 \AcceptanceTester\CRMOperatorSteps 類:
cept generate:stepobject acceptance CRMOperatorSteps
在對象被生成前,Codeception(譯註:做者此處筆誤爲Composer)會提示你輸入方法名。直接回車,就是告訴codeception你打算從新開始。
這個輔助器被用來支持StepObject模式(http://codeception.com/docs/07-AdvancedUsage#StepObjects),所以,它會自動添加Steps後綴到 CRMOperatorSteps 類名後。固然,把AcceptanceTester子類分紅不一樣的角色,比只是定義一些抽象的steps容器要更天然。然而,若是咱們強制重命名生成的類,刪除後綴,咱們將失去Codeception提供的自動加載能力,相比之下,咱們仍是忍受這種命名方式吧。CRMOperatorSteps.php 類會被放在 tests/acceptance/_steps 子目錄中。
咱們用一樣的方法生成 CRMUserSteps 類。
如今,讓咱們來定義以前提到測試場景的steps。幾乎全部的高級steps正好是Codeception內建的低級step的容器。
首先,咱們來看看CRMOpeator的steps。
「I am in Add Customer UI」step是一個完成添加客戶特性的開放路由,所以,代碼差很少像這樣:
function amInAddCustomerUi() { $I = $this; $I->amOnPage('/customers/add'); }
"Imagine Customer"是進入添加客戶界面後,自動隨機生成客戶數據的輔助方法。佔位數據能夠用任何方式來生成。咱們將使用一個使人吃驚的Faker庫(https://github.com/fzaninotto/Faker),來生成看起來真實的數據。稍後,我再來深刻分析一下。如今,須要在添加客戶的實際界面中錄入數據。咱們在這裏不去追求很炫的界面,只是一個帶提交按鈕的HTML表單就夠了。可是,哪些字段須要填充呢?讓咱們回到客戶模型,來看看哪些部分在測試場景中是必須填充的。
爲了簡化問題,咱們把電子郵件和地址留到之後處理。咱們也徹底沒有考慮聯繫人集合,一樣是出於簡化的目的。咱們包含了客戶全部的惟一部分:姓名、生日、備註。記住,姓名是一個結構,而不僅是像Notes同樣的文本行。
如今,讓咱們把注意力集中在添加客戶表單的字段上。請注意表單上的姓名字段,這不是任意指定的,而是跟咱們的將來數據庫結構以及Yii2的模型配置是一致的。讓咱們來看看這張表:
注意,雖然咱們的設計是客戶能夠有多個電話,但咱們只有一個也是容許的。咱們推薦不直接去實現一個特性,而是應該先爲它寫一個測試。咱們的測試沒有去明確檢查,容許存在多個電話的能力。
因此,咱們如今來定義 CRMOperatorSteps.imagineCustomer 方法。首先,咱們將 Faker 庫引入項目:
php composer.phar require "fzaninotto/faker:*"
而後,咱們用如下代碼來配置客戶的屬性:
public function imagineCustomer() { $faker = \Faker\Factory::create(); return [ 'CustomerRecord[name]' => $faker->name, 'CustomerRecord[birth_date]' => $faker->date('Y-m-d'), 'CustomerRecord[notes]' => $faker->sentence(8), 'PhoneRecord[number]' => $faker->phoneNumber, ]; }
這樣,咱們建立了一個很容易使用的結構,在 fillCustomerData 方法中,咱們能夠這樣使用:
function fillCustomerDataForm($fieldsData) { $I = $this; foreach($fieldsData as $key => $value){ $I->fillField($key, $value); } }
提交表單的操做就比較直接了當,咱們把按鈕命名爲Submit:
function submitCustomerDataForm() { $I = $this; $I->click('Submit'); }
而後,咱們須要兩個方法,一個是用來檢查咱們是否是處於客戶列表界面,另外一個是轉到客戶列表頁面:
public function seeIAmInListCustomersUi() { $I = $this; $I->seeCurrentUrlMatches('/customers/'); } function amInListCustomersUi() { $I = $this; $I->amOnPage('/customers'); }
在Codeception的概念中,斷言方法應該在方法名中帶有see前綴,因此咱們遵照了這一條約定。
咱們使用方法 CurrentUrlMatches 利用正則表達式來匹配URL,而不是採用更加嚴格的 CurrentUrlEquals,這是由於咱們假定在URL的尾部,還會含有一些查詢參數。
寫完這些定義在 CRMOperatorSteps 類中的方法,咱們首個測試用例就完成一半了(這意味着可運行了)。
讓咱們從CRM用戶視角,來作完整個測試,他們須要使用查詢功能。在 CRMUserSteps 類中,咱們須要寫以下代碼。首先,比較顯而易見的是:
function amInQueryCustomerUi() { $I = $this; $I->amOnPage('/customers/query'); }
讓咱們用在 添加客戶界面 中相同的命名方式,來命名 填充電話號碼字段 這個方法。
function fillInPhoneFieldWithDataForm($customer_data) { $I = $this; $I->fillField('PhoneRecord[number]', $customer_data['PhoneRecord[number]']); }
讓咱們將查詢客戶數據的按鈕命名爲Search:
function clickSearchButton() { $I = $this; $I->click('Search'); }
複製一下 CRMOperatorSteps.seeIAmInListCustomersUi:
function seeIAmInListCustomersUi() { $I = $this; $I->seeCurrentUrlMatches('/customers'); }
這是爲了讓咱們遵照 Refactoring: Improving the Design of Existing Code, Martin Fowler, Kent Beck, John Brant, William Opdyke, and Don Roberts, Addison-Wesley Professional 的第三規則。
最後,咱們來添加斷言:
function seeCustomerInList($customer_data) { $I = $this; $I->see($customer_data['CustomerRecord[name]'], '#search_results'); } function dontSeeCustomerInList($customer_data) { $I = $this; $I->dontSee($customer_data['CustomerRecord[name]'], '#search_results'); }
咱們須要注意,這個極其簡單的實現,是基於幾個在開發階段有效的假設的:
全部客戶都定義了姓名
沒有重名客戶
搜索結果呈如今id爲 search_results 的HTML元素中
讓咱們保持這個測試簡單,可是,當咱們有超過一個的搜索結果時,咱們須要思考怎樣正確檢測一條具體結果是否存在(最可能的是,see方法提供的缺省的see語義就不夠用了)。
一個很重要的問題是,爲何咱們不能在每新增一個客戶時,經過客戶列表的UI來檢測客戶數據。在咱們用電話號碼查詢後,畢竟咱們會獲得相同的客戶列表UI。
緣由很是簡單:咱們的目標是咱們能經過電話號碼查詢客戶。而且,中途存在的斷言會違反「單一斷言原則」(Clean Code, Robert Martin, Prentice Hall 裏有詳細的解釋)。然而,由於這是一個端到端的驗收測試,這樣作也並不是壞事。不管如何,沒什麼會妨礙咱們從此擴展這個測試(這只是一個模擬真實用戶行爲的端對端測試)。但如今,讓咱們保持簡單的場景。
若是你如今運行完整的測試場景,你可能會遇到下面的錯誤:
1)Failed to add two different customers to database in QueryCustomerByPhoneNumberCept
Sorry, I couldn't fill field "CustomerRecord[first_name]", "Cheyanne": Field by name, label, CSS or XPath 'CustomerRecord[first_name]' was not found on page.
Scenario Steps:
2. I fill field "CustomerRecord[first_name]", "Cheyanne"
1. I am on page "/customers/add"
遇到這些錯誤的緣由是,咱們尚未處理 /customers/add 請求。
下面咱們到了該安裝Yii2的時候了。
咱們打算完成一個完整的自定義應用,並不想依賴Yii框架的目錄結構,只要能方便的使用它提供的類就能夠了。
首先,在應用中聲明對Yii2的依賴。
手工在composer.json文件中加入require行,與執行下面的命令的做用是同樣的:
php composer.phar require "yiisoft/yii2:*"
若是你是手工編輯composer.json文件,記得還要運行安裝命令:
php composer.phar install
這樣,Composer會把Yii2安裝到你的代碼中,位於 vendor/yiisoft/yii2 目錄。
Yii2包含了一個重要的特性,提供了一個內建的需求環境檢查器。當你安裝在第一章中討論的應用模版時,在代碼的根目錄會有一個 requirements.php 的腳本。它很是有用,因此拷貝一份,粘貼到web子目錄中。你也能夠從Yii2代碼倉庫下載這個文件:https://github.com/yiisoft/yii2/blob/master/apps/basic/requirements.php。取得這個文件後,在命令行運行它:
php web/requirements.php
或者,你也能夠用瀏覽器訪問 http://<your_domain>/requirements.php 獲得一個更加友好的頁面,來查看部署環境是否知足框架的需求。
真正高層的說明以下所述。爲了服務發送到應用的請求,Yii實例化一個 \yii\web\Application 對象,它使用 MVC 模式來處理請求,返回結果。若是你忘記或是對MVC模式不熟悉,你可能須要閱讀一下Yii的官方文檔,爲後面更深的內容作準備。
Yii對 MVC 模式的解釋是:
View 是負責呈現的類,不管發送什麼到客戶端,都由它來展示。一般,是HTML頁面,但也不侷限於此
Model 是包含商業規則的類
Controller 是接受用戶請求,決定如何處理,若是必要,調用 model 進行實際處理工做,並使用 view 來呈現結果,將結果返回給用戶
這個模式最微妙的部分是 model 的概念。依照解釋,model 既能夠是 controller 用來獲取數據,再推送給 view,也能夠直接就是 controller 推送給 view。Yii2沒有作強制性規定,但 model 的實現假定它是數據容器,短暫(只在內存中)或持久(經過Active Record模式實現)存在。
所以,一個請求經歷了以下步驟:
web服務器接收到請求,傳遞給 index.php 腳本。
一個Yii的Application對象被建立。它決定使用哪一個Controller來處理請求。
一個Controller對象被建立。它決定使用哪一個Action來執行(能夠是Controller的方法,或者另外一個分離的類),將詳細的請求信息傳遞給Action來執行
action被執行,一般會經過view來返回結果。這不是框架強制性的要求,你也能夠不呈現任何東西。
在將結果返回給用戶以前,一個特殊的應用組件負責格式化數據。
結果數據,HTML或JSON或XML甚至是一個空的返回,發送給客戶
理解以上這些步驟,讓咱們修改當前的入口腳本,利用Yii框架而非直接輸出原始文本,來完成一樣的工做。咱們將在第12章,路由管理中看到更好更詳細的流程圖。
如今,咱們的項目結構以下圖所示:
咱們將從入口點腳本開始介紹Yii2。爲了簡化處理,index.php 文件看起來應該以下所示:
<?php // Including the Yii framework itself (1) require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php'); // Getting the configuration (2) $config = require(__DIR__ . '/../config/web.php'); // Making and launching the application immediately (3) (new yii\web\Application($config))->run();
在代碼(1)處,咱們將Yii框架引入了環境中。
在代碼(2)處,咱們引入了應用的配置文件。Yii應用的配置是一個巨大的PHP數組,包含應用的初始配置,以及衆多組件的配置。
在代碼(3)處,咱們建立了一個Application子類WebApplication的實例,並當即調用了run方法。
再回到代碼(2)處,咱們載入了一個並不存在的文件 config/web.php,讓咱們來實現它:
<?php return [ 'id' => 'crmapp', 'basePath' => realpath(__DIR__ . '/../'), 'components' => [ 'request' => [ 'cookieValidationKey' => 'your secret key here', ], ], ];
咱們必須詳細說明一下這三個設置:
id:這是應用的強制性標識符。它是必須的,咱們用它來跟應用的其它模塊區分開。頂層應用,是跟普通模塊聽從一樣的規則的。
basePath:這也是強制性的,由於對Yii來講,這是在文件系統中定位應用的基本方法。在其它地方設置的相對路徑,都是基於這裏設置的基礎路徑。
components.request.cookieValidationKey:這是用戶認證子系統的一個漏洞,咱們將在第5章用戶認證中進行討論。該項設置是一個私有key,用於「記住我」這個特性,依賴於cookies。在早期的Yii2的beta版中,這個key是自動生成的。從4e4e76e8提交能夠看到(https://github.com/yiisoft/yii2/commit/4e4e76e8838cbe097134d6f9c2ea58f20c1deed6)。除了這個設置項以外,你也能夠將components.request.enableCookieValidation設置爲false,這樣禁用基於cookie的認證。這樣,應用也能夠正常工做(譯註:若是這兩個設置項都沒有設置,請求將會顯示一個錯誤提示)
接下來,咱們將添加一些強制性的目錄,由於若是沒有這些目錄,Yii將拋出一些異常。注意,請不要建立 web/assets 和 runtime 目錄。這些目錄在應用運行時被框架使用。
每個控制器都應該具有如下三個特徵:
必須屬於在 Application 類的 controllerNamesapce 項定義的命名空間。
名稱中必須包含 Controller 後綴
必須是 \yii\base\Controller 的擴展類。當前示例是一個web應用,而不是一個控制檯應用,所以,咱們應從 \yii\web\Controller 繼承。
另外,這對理解Yii2實際查找控制器類很是重要。
在一般狀況下,Yii2利用一個兼容PSR-4標準的類自動裝載器(http://www.php-fig.org/psr/psr-4/)。爲了簡化處理,自動裝載器把命名空間做爲文件中的路徑,利用一個已經定義的特殊根命名空間,映射到代碼根目錄。
在咱們的案例中,Yii2爲咱們定義了 \app 這個命名空間,映射到代碼根目錄。controllerNamespace 設置項的缺省值就是 \app\controllers,映射到代碼根目錄下的 controllers 目錄,所以,全部的控制器都應該放在這裏。
採用這種機制,全部的類均可以經過Yii2的自動裝載器進行正確的加載。
如今,咱們來建立第一個控制器來經過冒煙測試。咱們不去改變缺省的控制器命名空間設置,只須要在 controllers/SiteController.php 文件中寫入以下代碼:
namespace app\controllers; use \yii\web\Controller; class SiteController extends Controller { public function actionIndex() { return 'Our CRM'; } }
這段代碼依賴了Yii的約定。不用深刻研究Yii的路由,咱們就知道,不進行特殊的設置時,Yii會調用 SiteController 控制器的 actionIndex 方法來處理「/」請求。
定義控制器action最簡單直接的方法,是將它做爲控制器的public方法,而且名稱帶有action前綴。顯式請求SiteController.actionIndex方法,應該請求 site/index.php。
冒煙測試經過了,讓咱們來添加一些調試用的輔助工具吧。
在開發階段,你可能會碰到各類奇葩的錯誤。讓咱們看看,有沒有辦法簡單快捷的對應用進行設置,收集儘量詳細的出錯信息。
首先,當你犯下一個可怕的錯誤時,好比沒有定義id或bathPath配置項,你基本上就會獲得一個空白頁,這時,你只能去查看web服務器的日誌。例如,在Apache中,你能夠可使用指令 ErrorLog 來指定錯誤報告文件,只要不是瀏覽器渲染階段的錯誤,均可以在這裏找到。
與「空白頁」鬥爭,你須要在 index.php 入口點中加入 display_errors 設置,放在 Yii 庫的後面,而且必須放在Application對象建立和執行的前面,代碼以下:
ini_set('display_errors', true);
一樣,你也能夠在引入Yii以前,添加一個方便的常量。在引入Yii以前加入,代碼放置的位置很是重要,由於若是你沒有定義,Yii會用缺省值定義它。代碼以下:
define('YII_DEBUG', true);
這將改變應用的調試模式,若是有異常拋出,一般會獲得500錯誤頁或空白頁面,但同時,詳細的錯誤報告會將最重要的行高亮顯示。
最後,你須要將添加自定義的日誌添加到應用中,這會將應用中的錯誤記錄到文件中。第8章,整體行爲,會給你一個詳細的解釋。