现在,我们拥有了可以工作的全套基础设施,让我们回到在设计阶段时定义的第一个特性,让我们先为它写一个验收测试。
端到端验收测试的要点就是,我们必须只通过UI来处理我们的应用。我们不能用任何方式去直接访问数据库,更糟糕的是,直接访问应用的文件系统。所以,测试一个数据库查询,数据应首先插入数据库。然后依靠UI来完成测试。
这里是结果测试的步骤:
打开添加客户数据到数据库的界面
添加第一个客户到数据库。你应该看到客户列表,只有一条记录
添加第二个客户到数据库。你应该看到客户列表里有两条数据了
打开通过手机号码查询客户的界面
用客户1的手机号码进行查询,你应该看到界面查询结果中有客户1,并且没有客户2
因此,测试强制我们提供三个页面:新建客户,客户列表,以及查询页面。这部分就是为什么我们要称之为”端到端测试“。
翻译成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章,总体行为,会给你一个详细的解释。
【翻译】Yii2 第2章 用Yii2创建自定义应用(第2节)
原文:http://my.oschina.net/u/870995/blog/512264