如今,咱们拥有了能够工做的全套基础设施,让咱们回到在设计阶段时定义的第一个特性,让咱们先为它写一个验收测试。
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章,整体行为,会给你一个详细的解释。