我们可以继续为上面的示例程序添加新的功能,但这样并不会帮助我们了解如何使用框架去开发一个真正的应用程序。为了做到这一点,我们需要建立一个更加紧密的逻辑,与真实的应用非常紧近的程序。
在这一章中,我们将介绍一个项目任务跟踪系统,给它取了个名字叫TrackStar。目前世界上,已经有很多关于项目管理和问题跟踪的应用程序,我们的基本功能将没有什么与众不同。那么,为什么还要建立呢?事实证明,这种基于用户的应用程序有很多的功能,也是很常见的网络应用,这将使我们能够实现两个主要目的:
TrackStar是一个软件开发生命周期(SDLC)中问题管理的应用软件。其实主要的目的是帮助保持在整个软件开发过程中所有出现的问题及问题的跟踪。这是一个基础于用户的应用程序,允许管理和创建用户及控制用户权限。一旦用户通过了验证和授权,将可以添加其他用户和管理项目。
TrackStar可以管理在项目开发中,用户与他人(通常是项目组成员)之间存在问题,如开发任务和一些应用程序的错误(bug),把这些问题,分配给项目组内的其他成员,该项目中的任务将有几个状态如:尚未开始,开始和结束。这样,TrackStar就可以准确的描述一个项目什么时候已经完成,当前正在进行什么,还有什么尚末开始。
简单说, User Stories就是应用程序的需求,并且根据应用程序的需求来确定User Stories。User Stories 最简单的形式是规定User可以使用应用程序的哪个部份。它们开始是很简单的,随着程序的复杂性会深入到每一个详细的功能。我们的目标是开始时确定足够的复杂性。如果需要,我们以后将会添加更多的细节和更多的复杂性。
我们之前已经谈到了这个应用程序包括三个主要的功能:用户(users)管理,项目(projects)管理和问题(issues)管理。在应用程序中,这几个主要功能是非常重要的。好了,让我们开始吧。
TackStar是一个基于用户的Web应用,它将有两个用户类型:
一个匿名用户是所有未能通过登录验证的用户。匿名用户只能访问注册或登录,其他受制限的功能需要验证用户。
一个认证用户是所有已经通守登录验证的用户。换句话说,已经认证的用户,他们将可以创建和访问应用程序的主要功能:项目管理和项目的问题管理。
在TrackStar中,项目管理是一个主要的功能。一个项目一般代表公司中高层人员的一个目标,并且由一个或多个人执行。典型的项目可以细分为更具体的任务(或问题),每个小步骤都代表需要完成这个项目的总体目标。
建立一个项目和和问题跟踪管理的应用,作为一个例子,它贯穿了整本书。很不幸,我们不能使用它来跟踪我们的程序,因为我们现在还没有开发。但是,假如我们使用一个类似的工具来帮助我们建立项目跟踪,我们可以创建一个项目叫建立TrackStart项目/问题管理工具。这个工具将项目分解成更细致的项目问题。例如:创建登录页面或设计数据库架构等。
经过身份验证的用户可以创建新的项目。该项目的创建者是这个项目的所有者。项目的所有者可以编辑和删除自己创建的项目,还可以为项目增加成员。除了项目所有者,其他与这个项目关连的用户简称为项目成员,项目成员可以添加新问题以及修改现有的问题。
项目中的问题可以分为以下三种类别:
问题(issues)可以有以下三种状态:
项目成员不仅可以添加新问题以及修改现有的问题。他们可以分配问题给自己或其他的项目成员。
现在,我们了解了这三个主要的模块就可以了,我们将更进一步研究,用户注册的细节以及如果在项目中添加一个任务,我们已经对基本的功能做了一个概述,下面我们将会更加注重细节,因为我们将要实现这些功能。
不过,在我们开始时,我们应该注意一下页面的导航和应用程序的流程。这将帮助我们建立更好的了解总体的布局和流程。
这是一件能概括应用程序主要页面以及它们是如何结合在一起的工作,这将有助于我们迅速找出所需的Yii控制器、操作方法和视图,以帮助我们建立期望实现的功能。
下图所显示的是一个基本的想法,描述了这个程序从登录开始到一个项目的细节列表的流程:
当用户第一次访问,必须进登录验证才能访问其他功能。一量登录成功,用户将看到一个与他相关的项目列表和一个创建新项目按钮,选定一个项目,将转到这个项目的详细信息页面。该项目的详细页面将显示各种类型问题的列表,并且也有添加新问题和编辑其他问题的按钮。
这都是一点非常基本的功能,但这个图片给了我们更多的信息是各各功能怎么样连接在一起,使我们能更好的确定需要的模型、视图和控制器。我们努力的结果是,这张图片可以与他人分享,并使每位参与者都能了解应用程序的流程。以我的经验,当要做一个新的应用程序时,几乎每个人都更喜欢看图并加以说明。
我们下面的工作是需要考虑一些有关的数据的事情,并开始建立规范。如果我们根据主要的功能划分,我们最终会得到一个不错的列表,使用Active Reocrd建立我们想要的数据模型。我们根据User Stories的设计,决定了以下内容:
根据User Stories的功能设计和工作流程图,我们首先尝试设计所需要的显示数据,如下图:
这是最基本的模型对象,描述了我们主要的数据实体,各自的属性,以及对象之间的一些关系。在1..*两边的对象,如Project(项目)对象和 User(用户)对象之间是多对多的关系。一个用户可以拥有多个项目,一个项目同样也拥有多个用户。同样,一个项目可以有0个或多个(Issues)问题,而与此相关的是Issues(问题)只属于一个项目。此外,用户是所有者(或请求者),可以有多个Issues(问题),但是Issues(问题)只有一个所有者(也只有一个请求者)。
我们一直在让这些属性保持简单。一个User(用户)只需要用户名和密码,以便可以登录。(Project)项目只需要一个名字。
根据我们当前了解,Issues(问题)是关联关系最多的。正如之前所说,Issues(问题)有一个分类属性,用来区分bug(错误)、 feature(功能)、task(任务)。他们也将有一个状态属性用来说明Issues(问题)的进展情况。用户在系统中刚刚创建Issues(问题) 时,requester(请求者就是用户自己),当把Issues(问题)分配给一个系统中的用户时,他将成为Issues(问题)的所有者。我们还定义了描述属性,用来详细说明这个Issues(问题)。
请注意,我们现在并没有明确的提到关于数据库的结构。事实上,直到现在我们才从数据的角度彻底全面的考虑我们真正需要什么。我们没有使用工作创建并存放这些数据。在操作系统中是否安装了关系数据库?我们是否需要持久化这些数据呢?
对这些问题的回答不需要在计划之初。因为这样可以更好的收集更多我希望和所需要的功能和数据,我们可以与项目相关者讨论要实现的细节,以确保一些想法是我们想要的。项目相关者包括开发人员,产品/项目经理等。他们总会给出一些建议和反馈。
就我们而言,我们实在不能与其他人做这方面的沟通。如果我们能够与你相见,我们一定会与你协商。不幸的是,以书本的这种格式,不允许双向沟通。因为,我们没有其他人来咨询,所以,我们按着之前的方法继续前进。
不过,在我们创建应用程序之前,我们需要了解一下我们的开发方式。我们在开始编码之前,将说明一些开发方式和原则。
我们将会使用敏捷的开发过程,提高复用性。现代的软件开发中,'Agile'(敏捷)是一个经常提到名词,在开发人员之间有不同的含义。我们在整个开发过程将会集中一个敏捷的方法,包括透明和公开的合作,不断的得到反馈,迅速响应不断变化的需求。
我们不能等到每个细节都确定后再开始编码,我们会逐步的进行。一旦功能的细节确定,我们将开始实现这个功能,其他的功能或细节仍在规划和设计阶段。
在实施中我们会重复的遵遁一个规则。我们首先会进行分析和设计,然后试着编写代码、测试代码,并收集反馈意见。设计(design)->编码 (code)->测试(test)->评价(evaluation),遵遁这个规则,直到每个人都很满意。一量大家都很满意,我们就可以将新的功能部署到我们的应用程序中,然后开始收集下一个功能设计,继续遵遁这个规则。
收集反馈信息是敏捷开发重要的部份。可以从用户、开发队团及所有项目干系人获得反馈。这个开发方式将允许它告诉你在应用程序整合或部署存在的问题。访方法经常由你来编写单元和功能测试得到反馈信息。
单元测试的编写提供了验证代码是否正确。功能测试的编写提供了对应用程序整体的功能是否正确。
单元测试是软件测试中最小的单位,在面向对象的应用程序中,(如Yii应用程序)的最小单位是类的接口,公共的方法。单元测试集中在一个单独的类中,而不要求与其它类或对象一起运行。他们的目的是为了验证一个最小单位的代码是否达到预期目的。
功能测试重点测试应用程序端对端的功能特生。这个测试相对于单元测试要高一个层次,通常要多个类或对象一起运行。功能测试的目的是验证一个给定的应用程序功能是否可以正常工作。
写单元和功能测试有许多好处。第一可以提供文档。单元测试可以准确迅速地找到一块代码中存的在问题。同样,功能测试文件可以测试应用程序内部的特性是否实现。如果你继续努力的编写这个测试文档,则这此文档的变化同样也是应用程序的变化。
测试文档也作为反馈机制,不继地向开发者和项目的其他干系人提供关于应用程序是否达到预期的工作。你每次修改代码后运行你的测试文档并获得即时反馈是否你无意中修改了该系统的其他行为,如果出现问题,立即解决这些问题。这增加了开发人员的信心,让应用程序中的错误减少,使项目更成功。
这样的即时反馈也有助于改进和改善代码的基础设计。开发者可能会改善现有的代码,如果测试文档写的很到位,应用程序的功能改变可以立即得到反馈。编写单元和功能测试文档,可增加开发人员的信息,编写更好的软件,发布更稳定的应用程序及高质量的产品。
测试驱动开发(TDD)是一种软件开发方法,它有助于为软件开发创造一个舒适和信心的环境,确保你的测试代码与你的应用程序一起成长,并始终保持最新。它规定在你开代码之前先写测试代码。下面是总结的步骤:
整个开发过程重复这些步骤。
即使再好的意图,如果你怕不及待的写下你的代码之后你可能不会完成测试代码。先写你的测试代码并在书写过程中再编写代码以保证最佳的测试覆盖率。这种深度的复盖有助于减少项目的压力,虽着应用程序的复杂度建立信心,并不断提供积极的反馈作为补充和作出改变。
为了使用TDD,我们需要了解Yii应用程序中如何测试。
从Yii1.1版本起,Yii已经紧密结合了PHPUnit(http://www.phpunit.de)和Selenium Remote Control(http://seleniumhq.org/projects/remote-control/)测试框架。特定的测试框架并没有关于 TDD,但非常推荐使用。
当然,你可以使用测试框架测试Yii的PHP代码。然而,Yii已经与前面提到的两个框架紧密集成,所以使事情变的更简单。因为简单,所以我们首要目标是使用Yii的测试功能。
在第2章中,当我们使用yiic webapp命令创建Hello,World应用程序时,我们注意到,我们创建了许多文件和文件夹。以下是自动测试相关的:
我们将我们的测试文件放到这个主要目录:fixtures,functional,unit。这些报告文件夹用于存储生成的代码覆盖率的报告。
PHP扩展XDebug,必须安装才能生成报告。推荐使用PECL安装XDebug。如果详细安装信息,请访问http://xdebug.org/docs/install。如果你想简单的跟随我们的例子,则这些不是必须的。
在Yii是中编写单元测试是从框架类CTestCase中继承一个PHP类。跟据约定,这个类被命名为AbcTest,其中Abc可以被替换成试测类的名称。例如,第2章的示例程序,如果我们要测试Message类,则测试类的名称是MessageTest。这个类是保存在protected/tests /unit/下,文件名是MessageTest.php。
测试类中主要包括测试的操作方法,名字是testXyz,其实Xyz经常与这个测试类相对的类的操作方法名称。
继续MessageController的例子,如果我们测试actionHelloworld()方法,则在MessageTest类中的测试方法是testActionHelloWorld()。
为了继续往下进行单元测试,您需要安装PHPUnit。应该使用Pear安装(获得更多关于Pear的信息,请访问http://pear.php.net),如Mac OS(苹果操作系统)有户,只需如下两条命令:
你的配置可能略有不同。了解更多安装过程的信息请访问:http://www.phpunit.de/manual/3.0/en/installation.html
关于PHPUnit测试功能使用已经超出了本书范围,建议你花一些时间阅读一下文档(http://www.phpunit.de/wiki/Documentation),并学习如何编写一个基础本的单元测试。
与单元测试一样,功能测试也是编写一个PHP类。但它是继承自己CWebTestCase类,而不是CTestCase类。跟据约定,如果我们的测试类名叫AbcTest,其中Abc是被测试的类,则测试类的文件名叫AbcTest.php只存在protected/tests/functional中。
为了运行功能测试,你需要安装Selenium。
除了PHPUnit,Selenium Remote Control Server (Selenium RC)是为了运行功能测试所需要的。安装Selenium Rc非常简单。
在解压后的目录中,有几个是基础于客户端的目录和一个包含Selenium RC Server的目录,它的名字有点类似 selenium-server-1.0.x/
其中的x是具体的下载版本。启动服务器也非常简单。只要在命令行下进入服务器文件所在目录,运行以下命令:
它将在命令行下启动服务。
我们将跟据TDD方式建立针对TaskStar应用程序的测试,主要是编写和执行单元测试。但是它现在还不能运行这个功能测试例子。我们在上面的 Hello, World应用中,创建一个site功能测试文件在 protected/tests/functional/SiteTest.php。这个文件中有三个方法。一个用来测试主页(Home),一个用来测试联系页(Contact),还有一个用来测试登录(Login)和注销(Logout)。
在我们运行这个功能测试这前,我们需要修改配置文件。首先,我们需要修改 protected/tests/WebTestCase.php中定义的测试URL地址,Selenium将尝试打开这个地址进行测试。找到 TEST_BASE_URL定义的URL,修改为上一章创建的项目,即以下定义:
修改为:
接下来的变化可能只适用于Mac OS(苹果操作系统)用户。尽管如此,如果你使用的是Windows系统,但不希望使用IE浏览器做为测试浏览器,那么你也需要进行修改。修改配置文件 protected/tests/phpunit.xml的Selenium部份,默认它被配置使用IE浏览器做为主浏览器。我们可以删除以下突出显示的一行代码,以确保Selenium运行时只使用Firefox。
现在,你已经安装了PHPUnit(如未安装请参阅前面的单元测试部分),并且Selenium服务器也已经运行了(如未运行,请参阅前面的Selenium部份),接下来我们在命令行下进入到测试目录,运行以下命令进行功能测试:
这将发生了什么?你将看到浏览器被自动调用,Selenium服务器使用浏览器访问,如同最终用户访问一样,访网站是我们在WebTestCase.php中配置的。它运行的这个测试方法实际上是自动的模拟网站真实用户的行为。很酷吧。
如果一切正常,最终结果应该显示在命令行窗口,我们执行的测试结果,类似下面的信息:
开始自动化质量保证测试(QA测试),能够自动完成最终用户的功能测试是一种很好的方式。如果这个项目你有一个独立的QA团队,你可以向他们演示如何使用这个工具来测试应用程序。如上所述,我们采用测试驱动(TDD)的方法,集中编写更多的单元测试,然后最终用户通过浏览器执行功能测试。随后的开发中我们使用一个测试套件来覆盖所有的单元和功能测试。
你运行SiteTest.php测试时,可能会失败。如果您的测试结果表明有一个位置失败,在SiteTest.php文件44行。这是由于主菜单显示的注销链接的名称,自动生成的测试文件编写的是Logout,而不是Logout (demo)。如果你的功能测试失败,只需更改注销链接,如果你使用的是demo/demo登录,则应该改成:
让我们简单地温习一下Hello World!示例,并为这个示例提供了一个TDD的测试方法。
我们有两个可工作的操作方法,一个显示Hello World!和一个显示Goodbye。这两个操作方法都在MessageController类中。
让我们为MessageController.php添加一个新的操作方法。让这个方法可以返回任何传进来的消息字符串。这听起来很简单,我们只需要在MessageController.php中添加一个公共的操作方法,可以叫repeat(),并让它完成我们之前所说的功能,这样做对吗?嗯,不太正确,由于我们要采用TDD方法,所以应该先为这个操作方法编写测试行为。
由于我们要测试这个类的操作方法的行为,所以需要编写一个单元测试。根据理力争Yii默认约定,这个单元测试文件应该放到protected/tests /unit/目录,并且文件名叫MessageTest.php。让我们放慢步骤,先添加这个类,并让它继承Yii框架中的单元测试类 CTestCase。
创建文件 protected/tests/unit/MessageTest.php 并添加如下代码:
现在,我们可以在命令行下进行此测试目录,并执行命令进行测试:
执行后将显示如下信息:
上面的信息告诉我们,测试失败,没有在MessageTest类中找到要测试的方法。的确如此,因为我们现在还没有编码。但是我们已经开始了TDD的第一步,快速编写一个失败的测试用例(尽管有人觉得我们并没有真正的编写测试用例)
让我们添加一个测试方法,由于我们要写一个用来验证MessageController类的repeat操作方法的测试用例,所以这个测试方法的名字叫testRepeat,并添加如下代码:
如果我们现在重新测试,将看到如下结果:
当然这是迈向正确的一步。下面我们继续遵循TDD,第二步编写足够的代码可以让测试通过。当然上面的测试用例已经通过了,那是因为此访问并没有测试任何内容,所以并没有用。尽管如此,这是迈向正确的一小步。由于这里并没有真正编写测试用例,下面让我们回到TDD流程的开始:快速编写一个失败的测试用例。
这一次我们将添加一些具体测试的代码,这些测试代码会测试MessageController中的repeat操作方法,为了做到这一点,我们需要:
现在让我们开始添加代码,testRepeart()方法的代码如下:
我们建新一个MessageController类的实例,并提供一个用来验证的controllerId传给构造函数。然后我们定义一个字符串变量$yell,并传给repeat()方法,并将返回结果保存在$returnedMessage变量中。
正如我们期望所返回的信息中包含完全相同的字符串。我们使用PHPUnit的API方法assertEquals()用来比较第一个字符串与第二个字符串的结果是否相等(或真或假)。现在这个测试用例已经写完了,让我们试着运行它:
你可能意识到这又回到了原点,但是,这的确是TDD的自然规律性。在正个测试的时间里,我们很快的建立测试用例,并让它先失败再通过。上面的这个错误告诉我们在创建一个MessageController实例时,并没有在classpath中找到这个类,所以我们需要在这个测试类中先包含 MessageController这个类。由于这个测试类是应用程序中的一部份,所以我们可以使用Yii::import语法来包含 MessageController类。
Yii::import方法允许我们快速包含一个定义的类。它不同与include和require,因为它很高效。这个定义类开始导入时其实并没有包含,直到它首次被引用时才包含。多次导入同一个定义类也要比include_once和require_once快的多。注意:当提到一个Yii框架内定义类,我们不需要导入或包含它,因为所有YIi的核心类已经预先导入了。
在MessageTest.php文件的顶部添加如下代码:
保存并运行这个测试,同样也出现了错误,但与之前的错误不同,其实这个错误是告诉我们调用的MessageController类中没有repeat()方法。这是对的,因为我们还没有添加这个方法。现在我们创建这个方法并让测试通过。
为MessageController添加如下方法:
保存并再次运行:
成功了!我们这个测试通过了。现在我们可以整理一下我们的测试代码,让结构更紧凑些:
你应该再执行一次测试,以确保它仍然可以通过。
如果你刚刚接触TDD,这一切看起来有点奇怪,特别是要考虑所有细节,有时可能很小的细节只是为了实现这样一个普通的方法。这主要是为了帮助你理解TDD 的流程和节奏。控制这个细节的大小是TDD的一门艺术。开始的细节可以很小,后面再放大,这样你会感觉很舒服也更加有信心。
提供给开发者几个在测试框架中需要了解的知识。测试结果通过以彩色编码显示。通过测试显示绿色,测试失败显示红色。这种方式也有点想马路上的红绿灯。 TDD常常强调:红(Red),绿(Green),重构(Refactor),重构(Repeat)。这是指的基本步骤,如下:
这一章介绍了一个任务跟踪程序TrackStar,我们将会在本书的后面章节讲解如何开发这个程序。我们谈论了这个程序的需求与功能,并用非正式的用户故事的形式提供了一些高层次的需求。然后,我们确定了一些主要的对象,我们将通过这些数据创建应用程序。
我们不仅讨论了将要创建什么,还概述了如何创建。我们讨论了一个基本的敏捷开发思想和方法,并将它应用到了我们自己的程序中。我们还提出了测试驱动开发 (TDD)为我们将要使用的敏捷开发方法之一。我们不仅讨论了抽象的TDD是什么,而且在Yii提供的测试框架中如何实现它。
在下一章中,我们终于离开了demo程序,开始编写这个应用程序TaskStar。