<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-3443179681250049771</id><updated>2011-11-01T23:08:19.643+08:00</updated><category term='Memory-Leaks'/><category term='AutoTest'/><category term='Ubuntu'/><category term='TDD'/><category term='Performance'/><category term='Optimize'/><category term='Ruby'/><category term='Patch'/><category term='Cucumber'/><category term='Rspec'/><category term='Rails'/><category term='Compile'/><title type='text'>Newbie On Rails</title><subtitle type='html'>方便初学Rails的朋友们快速上手并驶入BDD敏捷之道。</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://l404.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://l404.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>404</name><uri>http://www.blogger.com/profile/08660239939199305528</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/_JmYLjCQND0c/SXaZ4wfUELI/AAAAAAAAANg/iLlbANMP1F0/S220/8b1111f6-216a-3853-a396-ee4cff0a219c.jpg'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>9</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-3443179681250049771.post-2041345401682518124</id><published>2009-04-05T09:38:00.001+08:00</published><updated>2009-04-14T09:39:47.162+08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Cucumber'/><category scheme='http://www.blogger.com/atom/ns#' term='Rspec'/><category scheme='http://www.blogger.com/atom/ns#' term='TDD'/><category scheme='http://www.blogger.com/atom/ns#' term='Rails'/><title type='text'>使用Cucumber+Rspec玩转BDD(7)——测试重构</title><content type='html'>&lt;b&gt;&lt;br /&gt;### 温故知新 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;在前面的六个章节中，我们循序渐进地完善了一个用户帐号系统，这样的系统一般都会作为一个独立的模块交付。在交付这个模块之前，还需要进一步地做些重构工作。在这篇文章中，笔者将会围绕测试重构展开。&lt;br /&gt;&lt;br /&gt;源码下载：&lt;a target="_blank" href="http://github.com/404/bdd_user_demo"&gt;http://github.com/404/bdd_user_demo&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 主要内容 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1. 测试环境本地化；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2. 归类 steps；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 3. 用 Factory_girl 代替 fixtures；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 4. Steps Within Steps；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 5. Helpers&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 新建工作分支 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;$ git checkout -b refactoring001&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 本地化测试环境 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;$ gedit lib/tasks/cucumber.rake&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;修改第 5 行，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp; t.cucumber_opts = "--format pretty --language zh-CN"&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;然后运行测试的时候就可以不用指定语言参数了，系统可以自动识别并读取我们用简体中文编写的故事。&lt;br /&gt;&lt;br /&gt;（不过，目前的 Cucumber 中，如果你编写的故事场景名称不是英文，好像不能识别场景名称；所以，在测试单个文件（*.feature）的时候，还需要加上 -l 参数。）&lt;br /&gt;&lt;br /&gt;可以运行下面的命令检测设置是否生效。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ rake features&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SePljqFuoeI/AAAAAAAAAe0/4fhJJzB6zng/user_demo_ok_50.png" /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 组织结构良好的测试脚本 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Cucumber 默认会加载 features/step_definitions/ 这个目录中所有的 *_steps.rb，如果读者朋友们稍微留心一点，就会很容易察觉到将一些公用方法放在 user_steps.rb 中算不上明智之举，因为那样的话感觉 user_steps.rb 有些杂乱。&lt;br /&gt;&lt;br /&gt;明智的做法是将这公用方法单独放入一个 *_steps.rb 文件中。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/step_definitions/page_steps.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;从 user_steps.rb 中剪切如下代码并作为 page_steps.rb 的填充，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;When /^我来到(.+)$/ do |page_name|&lt;br /&gt;&amp;nbsp; visit path_to(page_name)&lt;br /&gt;end&lt;br /&gt;&amp;nbsp;&lt;br /&gt;When /^我在输入框&amp;lt;(.+)&amp;gt;中输入&amp;lt;(.*)&amp;gt;$/ do |field, value|&lt;br /&gt;&amp;nbsp; fill_in(field, :with =&amp;gt; value)&lt;br /&gt;end&lt;br /&gt;&amp;nbsp;&lt;br /&gt;When /^我勾选&amp;lt;(.+)&amp;gt;$/ do |field|&lt;br /&gt;&amp;nbsp; check(field)&lt;br /&gt;end&lt;br /&gt;&amp;nbsp;&lt;br /&gt;When /^我按下&amp;lt;(.+)&amp;gt;按钮$/ do |button|&lt;br /&gt;&amp;nbsp; click_button(button)&lt;br /&gt;end&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Then /我应该看到&amp;lt;(.+)&amp;gt;的提示信息/ do |msg|&lt;br /&gt;&amp;nbsp; response.body.should =~ Regexp.new(msg)&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;开发人员应该尽量拆散一些杂乱的测试脚本文件，使得每一个测试脚本文件看起来干干净净，清晰明了。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### Factory_girl 初步之装载测试数据 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Factory_girl 是一个绝佳的fixtures替代品。fixtures 即Rails单元测试中内置的测试夹具，用来放一些测试数据；以前用单元测试构建测试数据的时候，都是在 test/fixtures/ 目录中新建YAML文件，并在这些YAML文件中按YAML的语法格式编写测试数据。而用上 Factory_girl 后，你可以直接用Ruby的语法编写测试数据；一是加快了测试速度，二来对程序员的大脑也友好些；而且 Factory_girl 能做的不仅仅是填充一些测试数据，还可以对这些数据进行灵活的变换以适应开发人员的需要。更多 Factory_girl 的信息请查阅该项目在 GitHub 上的主页：&lt;a target="_blank" href="http://github.com/thoughtbot/factory_girl"&gt;http://github.com/thoughtbot/factory_girl&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;可以用 gem 命令安装 factory_girl，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gem install thoughtbot-factory_girl --source http://gems.github.com&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;然后在你的环境配置文件中绑定这个gem包。由于factory_girl是拿来做测试用，所以将该gem包绑定在测试环境的配置文件中。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit config/environments/test.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;添加如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; config.gem "thoughtbot-factory_girl", :lib =&amp;gt; "factory_girl", :source =&amp;gt; "http://gems.github.com"&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;按照文档上所说的，Factory_girl可以自动加载建立在 test/ 和 spec/ 目录中的测试数据。我们不妨在 spec/ 目录中新建一个 factories 目录，并在这个目录中放置所需的测试数据。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ mkdir spec/factories&lt;br /&gt;&lt;br /&gt;$ gedit spec/factories/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;填充如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;Factory.define :static_user, :class =&amp;gt; User do |user|&lt;br /&gt;&amp;nbsp; user.username&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { '404' }&lt;br /&gt;&amp;nbsp; user.email&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { 'xuliicom@gmail.com' }&lt;br /&gt;&amp;nbsp; user.password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { 'password' }&lt;br /&gt;&amp;nbsp; user.password_confirmation { 'password' }&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;如上，我们定义了一个 Factory，这个 Factory 的名字叫 :static_user， :static_user 代表的是一个instance；在 Factory.define 的第二个参数中，我们用迭代器的方式构造了一个User 模型类的实例，这个实例可以通过 :static_user 来标识。&lt;br /&gt;&lt;br /&gt;接下来，我们要在测试代码中调用 :static_user 这个 instance 所包含的内容（即测试数据）。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit spec/models/user_spec.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;找到如下这段代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; before(:each) do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @valid_attributes = {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :username&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; '404',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :email&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'xuliicom@gmail.com',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'password',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password_confirmation&amp;nbsp; =&amp;gt; 'password'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.new(@valid_attributes)&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;将 before(:each) do ... end 这段代码替换如下，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; before(:each) do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = Factory.build(:static_user)&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;上面两段代码的效果是一样的。使用第二种方式，我们将测试数据建立在了测试代码之外，并通过一行代码就将测试数据搬到测试脚本中来了，我们可以在任何测试文件中以这样的方式来“搬运”测试数据。显然，读者朋友们已经尝到了使用Factory_girl的第一个甜头，那就是测试数据 “一次定义，多处可用”。上面的代码中，Factory.build 创建了我们在用测试数据填充的 UserModel 实例对象。如果省去 build 方法直接用 Factory(:static_user) 这种形式还会多一个save操作，不过在此我们只需要在测试的时候内容中有这么一个数据就行了，所以才用 build 方法。&lt;br /&gt;&lt;br /&gt;接着，找到如下这段代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; it "should have a unique username and password" do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @first_user = User.create!(@valid_attributes)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user = User.new(@valid_attributes)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user.should_not be_valid&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user.should have(1).errors_on(:username)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user.should have(1).errors_on(:email)&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;修改为，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; it "should have a unique username and password" do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#666666"&gt;@first_user = Factory.create(:static_user)&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#666666"&gt;@second_user = @user&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user.should_not be_valid&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user.should have(1).errors_on(:username)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user.should have(1).errors_on(:email)&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;Factory.create() 和 Factory() 方法都会新建记录并执行保存操作，然后返回保存后的实例对象；另外，Factory()方法还可以传入hash参数修改已定义的属性值。在上面修改后的代码中，@first_user 指向的是一个已经保存过的User实例，@second_user试图使用同样的数据新建同样的记录；记得前面我们在 UserModel 类中加入过 username 和 email 必须唯一的验证，那么理论上@second_user的行为应该不会得逞，所以 @second_user.should_not be_valid 及后面两句断言应该能够顺利运行，不妨运行下测试看看。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/spec spec/models/user_spec.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SePmRX5BufI/AAAAAAAAAfA/ma0bQI2i4mQ/user_demo_ok_49.png" /&gt;&lt;br /&gt;&lt;br /&gt;测试通过！我们再来找找其他的测试代码可以用上 Factory 的地方。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/step_definitions/user_steps.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;找到如下这段代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;Given /^我已经使用&amp;lt;(.*)\/(.*)\/(.*)&amp;gt;注册过(且已经激活了帐号)?$/ do |username, email, password, confirm|&lt;br /&gt;&amp;nbsp; @valid_attributes = {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; :username&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; username, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; :email&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; email, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; password, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password_confirmation =&amp;gt; password&lt;br /&gt;&amp;nbsp; }&lt;br /&gt;&amp;nbsp; @user = User.create!(@valid_attributes)&lt;br /&gt;&amp;nbsp; if confirm&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.activation_token = nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.save(false)&lt;br /&gt;&amp;nbsp; end&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;修改为，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;Given /^我已经使用&amp;lt;(.*)\/(.*)\/(.*)&amp;gt;注册过(且已经激活了帐号)?$/ do |username, email, password, confirm|&lt;br /&gt;&lt;font color="#666666"&gt;&amp;nbsp; @user = Factory :static_user,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; :username&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; username,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; :email&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; email, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; password,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password_confirmation =&amp;gt; password&amp;nbsp; &lt;/font&gt;&lt;br /&gt;&amp;nbsp; if confirm&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.activation_token = nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.save(false)&lt;br /&gt;&amp;nbsp; end&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;在上面的那段代码中，我们用测试脚本中的参数结合Factory() 方法修改了 :static_user 这个实例的属性值。&lt;br /&gt;&lt;br /&gt;运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ rake features&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SePljqFuoeI/AAAAAAAAAe0/4fhJJzB6zng/user_demo_ok_50.png" /&gt;&lt;br /&gt;&lt;br /&gt;测试通过！其实这段代码我们还可以进行重构，因为 if confirm .. end 中间还有两行代码，而且其中有一行代码还是赋值操作，不仅修改了数据，后一句代码还对修改的数据记录执行了更新操作；这就是说我们在测试脚本中用ActiveRecord的方式操作了数据。那么，既然是跑测试，为了测试脚本的干净利落，我们是不是应该尽量减少使用测试脚本直接地操作数据，转而用定义测试数据的方式实现呢？如果是，那么像这样一个“问题”您能想到好的解决方案吗？暂且留给读者朋友们思考，如果您有好的想法欢迎在下面留言！&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### Steps Within Steps ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;正如我们在第6章所说的那样，Cucumber的运行以故事场景为单位，这些故事场景都是彼此独立的；上一个场景的执行结果不会在下一个（或其他）场景中有效。如果要在场景B中用到场景A中的情节步骤，就需要在场景B中重复定义场景A所用的情节（至少在测试代码里边包含针对这一复用情节的相关脚本）。我们在前面编写的故事用例中，大多数场景都包含相同的情节，比如下面这句：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;当 我以&amp;lt;xuliicom@gmail.com/password&amp;gt;这个身份登录&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;这个场景子句还是一组复合语句，即 steps within steps 嵌套模式，可以打开 features/step_definitions/user_steps.rb 文件查阅这个子句的具体实现。如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;When /^我以&amp;lt;(.+)\/(.+)&amp;gt;这个身份登录(并勾选&amp;lt;记住我&amp;gt;)?$/ do |username_or_email, password, remember|&lt;br /&gt;&amp;nbsp; 当 %{我来到用户登录页面}&lt;br /&gt;&amp;nbsp; 而且 %{我在输入框&amp;lt;用户名或邮箱&amp;gt;中输入&amp;lt;#{username_or_email}&amp;gt;}&lt;br /&gt;&amp;nbsp; 而且 %{我在输入框&amp;lt;密码&amp;gt;中输入&amp;lt;#{password}&amp;gt;}&lt;br /&gt;&amp;nbsp; 而且 %{我勾选&amp;lt;记住我&amp;gt;} if remember&lt;br /&gt;&amp;nbsp; 而且 %{我按下&amp;lt;登录&amp;gt;按钮}&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;上面这段代码折叠后仅仅一行而已，如果不折叠直接放到故事场景中去，又加上又是频繁使用的故事情节，想必会增加一些工作量。当功能越来越多的时候，如果不使用 steps within steps 这种模式，我们的手指就得多敲击几次键盘，故事的行数也会明显增加而且增加的都是相同步骤，那样也坏了DRY（Don't Repeat Youself）的规矩。所以，建议在编写一些出现频率较高的故事情节时，适当地折叠一下！&lt;br /&gt;&lt;br /&gt;举个例子，用户在站点的很多操作（比如修改个人资料，发帖等等）都必须是在线状态，用一句话概况就是 “先登录，后操作”。“操作”有若干项，在操作之前，用户登录一次即可。这里的“操作”好比一些将要开发的新功能（比如发帖等）。那么在编写故事用例的时候，场景中免不了用户登录这一情节。结合前面我们编写故事场景的方式，发贴的某个场景应该像这样定义，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 登录用户发帖&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过且已经激活了帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我以&amp;lt;xuliicom@gmail.com/password&amp;gt;这个身份登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;登录成功&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该成功登录网站&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我来到发布新帖页面&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在...&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ...&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;这个场景中仅仅构造用户登录的就有4个子句，即4个步骤或者说4个情节。用一个词来形容，繁琐。当新增的功能越来越多，登录用户要操作这些功能的时候，我们要编写的故事场景个个都会肥胖无比。之所以把需求搬上测试上，就是要让需求更易懂且可行。所以，需要瘦身！“减肥”后的故事如下，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 登录用户发帖&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#666666"&gt;假如 我已经登录&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我来到发布新帖页面&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在...&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ...&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;这时候虚拟出一个已经登录的用户仅仅只有一行字而已。正所谓浓缩的才是精华，下面笔者就来揭示浓缩精华的秘密。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/step_definitions/user_steps.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;添加如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;Given /^我已经登录$/ do&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit spec/factories/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;添加如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;Factory.sequence :username do |n|&lt;br /&gt;&amp;nbsp; "test_user#{n}"&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;Factory.sequence :email do |n|&lt;br /&gt;&amp;nbsp; "test_user#{n}@example.com"&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;Factory.define :user do |user|&lt;br /&gt;&amp;nbsp; user.username&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { Factory.next :username}&lt;br /&gt;&amp;nbsp; user.email&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { Factory.next :email }&lt;br /&gt;&amp;nbsp; user.password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { 'password' }&lt;br /&gt;&amp;nbsp; user.password_confirmation { 'password' }&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;Factory.sequence 会生成序列，即给传入的参数构造唯一值。在上面的代码中，我们分别给 :username 和 :email 创建了序列，那么当用 Factory.next 访问的时候，每次 :username 和 :email 的值都是不同且唯一的，这更接近真实世界中的（注册）行为。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/step_definitions/user_steps.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;修改 Given /^我已经登录$/ do end 如下，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;Given /^我已经登录(并勾选&amp;lt;记住我&amp;gt;)?$/ do |remember|&lt;br /&gt;&amp;nbsp; @user = Factory(:user)&lt;br /&gt;&amp;nbsp; @user.activation_token = nil&lt;br /&gt;&amp;nbsp; @user.save(false)&lt;br /&gt;&amp;nbsp; 当 %{我以&amp;lt;#{@user.email}/#{@user.password}&amp;gt;这个身份登录#{remember}}&lt;br /&gt;&amp;nbsp; 那么 %{我应该看到&amp;lt;登录成功&amp;gt;的提示信息}&lt;br /&gt;&amp;nbsp; 而且 %{我应该成功登录网站}&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;尽管这种写法看起来稍微丑陋，一半是数据操作，另一半是嵌套的情节调用；但却是简化了不少操作，尤其是用户注册和激活功能。不妨来实际应用一下，看看是否生效。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/user_logout.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;修改后的故事用例如下，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;功能: 用户安全退出&lt;br /&gt;&amp;nbsp; 1.提供一个“退出”链接，用户登录后点击该链接可以注销在线状态；&lt;br /&gt;&amp;nbsp; 2.用户登录并勾选记住我后，点击“退出”链接可以注销在线状态，下次访问的时候将不再自动登录。&lt;br /&gt;&lt;br /&gt;&amp;nbsp; 场景: 用户注销在线状态&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#666666"&gt;假如 我已经登录&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我退出网站&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;您已经安全退出&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该尚未登录&lt;br /&gt;&lt;br /&gt;&amp;nbsp; 场景: 用户在持久在线状态下退出&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#666666"&gt;假如 我已经登录并勾选&amp;lt;记住我&amp;gt;&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我退出网站&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;您已经安全退出&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该尚未登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我关闭网页下次再来访问的时候&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该尚未登录&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_logout.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SePnEQ6HHCI/AAAAAAAAAfM/yFRpIbqfWyM/user_demo_ok_51.png" /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 给测试脚本加上 Helper ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;user_steps.rb 文件中 “ Given /^我已经登录(并勾选&amp;lt;记住我&amp;gt;)?$/ {...} ” 这段脚本嵌套了其他的 steps 语句，其中有些 steps 还包含其他 steps 子句。很容易看出这些 steps 嵌套的层级较深，这样会聚合大量的正则匹配操作，这些正则匹配是计算时间成本的，多了有损测试速度。所以可以考虑将这些 steps 打回原形，直接用编码实现。如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;Given /^我已经登录(并勾选&amp;lt;记住我&amp;gt;)?$/ do |remember|&lt;br /&gt;&amp;nbsp; @user = Factory(:user)&lt;br /&gt;&amp;nbsp; @user.activation_token = nil&lt;br /&gt;&amp;nbsp; @user.save(false)&lt;br /&gt;&lt;font color="#666666"&gt;&amp;nbsp; visit login_path&lt;br /&gt;&amp;nbsp; fill_in "用户名或邮箱", :with =&amp;gt; @user.email&lt;br /&gt;&amp;nbsp; fill_in "密码", :with =&amp;gt; @user.password&lt;br /&gt;&amp;nbsp; check "记住我" if remember&lt;br /&gt;&amp;nbsp; click_button "登录"&lt;br /&gt;&amp;nbsp; response.body.should =~ /登录成功/&lt;br /&gt;&amp;nbsp; request.session[:user_id].should_not be_nil&lt;/font&gt;&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存。运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ rake features&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SePnV6jqk7I/AAAAAAAAAfY/EavoA2uoSMg/user_demo_ok_52.png" /&gt;&lt;br /&gt;&lt;br /&gt;测试通过！在常见的WEB应用用，用户在执行某些操作前一般都要求登录。将这些操作对应到测试脚本中，所以登录行为很容易地被看成公共的steps。前面讲过，应该尽量将公共操作放到指定的测试脚本中；因此，上面那段代码还可以再灵活些。只不过，这次会介绍一种新的方式，即使用 Cucumber 的 Helper 模式。&lt;br /&gt;&lt;br /&gt;前面我们在用 Factory_girl 组织测试数据的时候学习到了一个好处，那就是 “一次定义，多处使用” 。这个理念非常经典，同样也是 Rails 提倡的 DRY 的原则之一，Cucumber 的 Helper 也体现了这一经典妙用！&lt;br /&gt;&lt;br /&gt;Cucumber 开放了一个接口，可以集成开发人员以Module方式组织的辅助方法（helper methods），如同在 Rails 中编写 helpers 一样。在 Rails 的 ApplicationHelper 模块中编写的辅助方法可以在任何页面模板中使用；在 Cucumber 中编写的 helper methods 则可以在Cucumber的任何测试脚本（*_steps.rb）中使用。编写辅助方法的详细教程可参阅：http://wiki.github.com/aslakhellesoy/cucumber/a-whole-new-world&lt;br /&gt;&lt;br /&gt;下面，笔者来演示这样一个例子。&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;$ gedit features/step_definitions/user_steps.rb&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;修改 Given /^我已经登录(并勾选&amp;lt;记住我&amp;gt;)?$/ do end 这段代码如下，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;Given /^我已经登录(并勾选&amp;lt;记住我&amp;gt;)?$/ do |remember|&lt;br /&gt;&amp;nbsp; &lt;font color="#666666"&gt;test_login!(remember)&lt;/font&gt;&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;def test_login!(remember = nil)&lt;br /&gt;&amp;nbsp; user = Factory(:user)&lt;br /&gt;&amp;nbsp; user.activation_token = nil&lt;br /&gt;&amp;nbsp; user.save(false)&lt;br /&gt;&amp;nbsp; visit login_path&lt;br /&gt;&amp;nbsp; fill_in "用户名或邮箱", :with =&amp;gt; user.email&lt;br /&gt;&amp;nbsp; fill_in "密码", :with =&amp;gt; user.password&lt;br /&gt;&amp;nbsp; check "记住我" if remember&lt;br /&gt;&amp;nbsp; click_button "登录"&lt;br /&gt;&amp;nbsp; response.body.should =~ /登录成功/&lt;br /&gt;&amp;nbsp; request.session[:user_id].should_not be_nil&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存 user_steps.rb。正常情况下，这时候要死运行测试应该会成功。&lt;br /&gt;&lt;br /&gt;接下来得将 test_login! 这个方法注册到 helper methods 中去，让任意 *_steps.rb 文件中的脚本都可以调用。&lt;br /&gt;&lt;br /&gt;先删除 user_steps.rb 文件中的 test_login! 方法，我们会在 Helper 中重新定义。&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;$ gedit features/support/user_helpers.rb&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;module UserHelpers&lt;br /&gt;&lt;br /&gt;&amp;nbsp; def test_login!(remember = nil)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; user = Factory(:user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; user.activation_token = nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; user.save(false)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; visit login_path&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; fill_in "用户名或邮箱", :with =&amp;gt; user.email&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; fill_in "密码", :with =&amp;gt; user.password&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; check "记住我" if remember&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; click_button "登录"&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; response.body.should =~ /登录成功/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; request.session[:user_id].should_not be_nil&lt;br /&gt;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;World { |world| world.extend(UserHelpers) }&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;这样Cucumber 运行时，World 将会包含 UserHelpers 模块中的方法，即 test_login! 成了可以在任意测试文件中（*_steps.rb）公用的辅助方法。&lt;br /&gt;&lt;br /&gt;保存 features/support/user_helpers.rb。运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_logout.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SePnEQ6HHCI/AAAAAAAAAfM/yFRpIbqfWyM/user_demo_ok_51.png" /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 小结 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;参与重构工作往往可以让开发人员在意识上踏上一个新的台阶，尤其是对于新手，更是眼前一亮，醍醐灌顶，乃至获得经验上的提升。往后的测试中，想必读者朋友们已经学到如何归类组织steps以及适当地折叠；还知道在什么情况下如何编写 Helper；当然，需要的话，别忘了捎上几名工厂妹（Factory_girl）。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 提交工作成果到GIT仓库 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;$ git status&lt;br /&gt;$ git add .&lt;br /&gt;$ git commit -m "First refactoring."&lt;br /&gt;$ git checkout master&lt;br /&gt;$ git merge refactoring001&lt;br /&gt;$ git branch -d refactoring001&lt;br /&gt;$ git tag v7&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;（注意，真正的开发中可不是到功能开发完毕了才commit，而是边开发边add和commit。为了方便演示编码过程，文章中没有一一列举。）&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/3443179681250049771-2041345401682518124?l=l404.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://l404.blogspot.com/feeds/2041345401682518124/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=3443179681250049771&amp;postID=2041345401682518124' title='1 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/2041345401682518124'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/2041345401682518124'/><link rel='alternate' type='text/html' href='http://l404.blogspot.com/2009/04/cucumberrspecbdd7.html' title='使用Cucumber+Rspec玩转BDD(7)——测试重构'/><author><name>404</name><uri>http://www.blogger.com/profile/08660239939199305528</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/_JmYLjCQND0c/SXaZ4wfUELI/AAAAAAAAANg/iLlbANMP1F0/S220/8b1111f6-216a-3853-a396-ee4cff0a219c.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://lh4.ggpht.com/_JmYLjCQND0c/SePljqFuoeI/AAAAAAAAAe0/4fhJJzB6zng/s72-c/user_demo_ok_50.png' height='72' width='72'/><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-3443179681250049771.post-3861886344550110296</id><published>2009-03-22T15:42:00.003+08:00</published><updated>2009-04-02T16:04:54.463+08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Cucumber'/><category scheme='http://www.blogger.com/atom/ns#' term='Rspec'/><category scheme='http://www.blogger.com/atom/ns#' term='TDD'/><category scheme='http://www.blogger.com/atom/ns#' term='Rails'/><title type='text'>使用Cucumber+Rspec玩转BDD(6)——找回密码</title><content type='html'>&lt;b&gt;&lt;br /&gt;### 温故知新 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href="http://l404.blogspot.com/2009/03/cucumberrspec5.html"&gt;用户登出&lt;/a&gt;后，过了一段时间再次登录的时候，有时候会忘记密码，这时候系统就得有个找回密码的功能，可以让用户在不用登录的情况下重设密码。对于一个存在的帐号，有且只有一个用户可以修改密码，这个用户必须是此帐号的拥有者；那么，系统怎么知道这个用户就是该帐号的所有者呢？&lt;br /&gt;&lt;br /&gt;答案是通过用户注册时填写的电子邮件来重建帐号和用户之间的关联。试想，如果一个用户曾经注册过，他必须填写了有效的电子邮件地址，而且还通过这个邮箱激活过帐号。那么，当注册用户忘记密码后，我们依然可以借助用户注册时填写的邮箱如法炮制，即发送一封带有重设密码链接的邮件，这个链接是唯一的，同样具有有效期，如果用户查收邮件并在有效的时间内访问了那个链接，那么将用户导向重设密码的页面，用户就可以更新密码了。&lt;br /&gt;&lt;br /&gt;为了获得更好的阅读体验，读者朋友们可以在这里下载源码：&lt;a target="_blank" href="http://github.com/404/bdd_user_demo/tree/master"&gt;http://github.com/404/bdd_user_demo/tree/master&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 新建工作分支 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ git checkout -b reset_password&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 找回密码功能 ### &lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; 1.用户点击“忘记密码？”链接，来到忘记密码页面；&lt;br /&gt;&amp;nbsp; 2.用户在忘记密码页面的输入框中输入注册时用的邮箱后，系统发送一封找回密码的邮件到该邮箱中；&lt;br /&gt;&amp;nbsp; 3.用户点击邮件中重设密码的链接来到重设密码页面；&lt;br /&gt;&amp;nbsp; 4.用户在重设密码页面输入新密码，确认无误提交后，系统将此新密码更新为用户的密码。&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 编写故事用例 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/reset_password.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;经过前面几次的练习，读者朋友们想必已经具备使用Cucumber玩转Testing的实际开发经验了。笔者下面就不一一去写和测试每一个故事了（如果还不是很熟练，可以参考&lt;a href="http://l404.blogspot.com/2009/03/cucumberrspec3_6419.html"&gt;第3章&lt;/a&gt;的介绍的迭代开发方式），而是直接将可能出现的场景全部写了下来。&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;功能: 重设密码&lt;br /&gt;&amp;nbsp; 为了用户在忘记密码后能够找回密码&lt;br /&gt;&amp;nbsp; 作为一个注册用户&lt;br /&gt;&amp;nbsp; 我应该可以重设密码&lt;br /&gt;&amp;nbsp; &lt;br /&gt;&amp;nbsp; 场景: 未注册用户请求重设密码&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 没有&amp;lt;somebody@somedomain.com&amp;gt;这个用户&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我使用邮箱&amp;lt;somebody@somedomain.com&amp;gt;来找回密码&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;没有找到somebody@somedomain.com这个用户&amp;gt;的提示信息&lt;br /&gt;&lt;br /&gt;&amp;nbsp; 场景: 注册用户请求重设密码&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过且已经激活了帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我使用邮箱&amp;lt;xuliicom@gmail.com&amp;gt;来找回密码&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;邮件发送成功！&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 应该有封重设密码的邮件发送至&amp;lt;xuliicom@gmail.com&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp; 场景: 用户重设密码但确认密码输入错误&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过且已经激活了帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我使用邮箱&amp;lt;xuliicom@gmail.com&amp;gt;来找回密码&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我访问&amp;lt;xuliicom@gmail.com&amp;gt;邮件中重设密码的链接&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我更新密码为&amp;lt;newpassword/wrong_password&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;密码更新失败&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该尚未登录&lt;br /&gt;&lt;br /&gt;&amp;nbsp; 场景: 用户成功重设密码&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过且已经激活了帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我使用邮箱&amp;lt;xuliicom@gmail.com&amp;gt;来找回密码&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我访问&amp;lt;xuliicom@gmail.com&amp;gt;邮件中重设密码的链接&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我更新密码为&amp;lt;newpassword/newpassword&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;密码更新成功&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该成功登录网站&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我退出网站&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我以&amp;lt;xuliicom@gmail.com/newpassword&amp;gt;这个身份登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该成功登录网站&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;在上面一系列的故事用例中，我们反复定义了 “当 我使用邮箱&amp;lt;xuliicom@gmail.com&amp;gt;来找回密码” 这一情节步骤。这里解释一下缘由：我们编写的每个故事场景都是相互独立的，这意味着上一个场景返回的结果不会在下一个场景中有效；如果要在下一个故事场景中用到上面故事场景中的步骤，就需要在这个故事场景中重复定义上面场景所用的情节（至少在测试代码里边包含针对这一重复情节的相关脚本）；这里复用的情节好比是函数中的局部变量，其作用域只在当前故事情节中有效而已。每一个故事场景的运行或多或少的覆盖了用于和用户产生交互的终端页面，包括处理业务逻辑的控制器，甚至和数据模型交互这些，相当于一次集成测试；我们编写的下一个故事场景会在上一个故事的基础上添加一些情节，通过运行这一系列连贯的故事场景，我们的开发工作在覆盖已有功能的基础上跟随这些迭代的集成测试同行。这也正是“测试-驱动-开发”的威力所在。感谢Cucumber的故事运行机制能够如此方便地帮助我们做到这点！&lt;br /&gt;&lt;br /&gt;保存 reset_password.feature。运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/reset_password.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SdRov1YeMNI/AAAAAAAAAc8/Ft29r30AuPs/user_demo_error_42.png" /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 编写故事用例运行所需的测试脚本 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/step_definitions/user_steps.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;添加如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;# reset password&lt;br /&gt;&lt;br /&gt;When /^我使用邮箱&amp;lt;(.+)&amp;gt;来找回密码$/ do |email|&lt;br /&gt;&amp;nbsp; 当 %{我来到找回密码的页面}&lt;br /&gt;&amp;nbsp; 而且 %{我在输入框&amp;lt;email&amp;gt;中输入&amp;lt;#{email}&amp;gt;}&lt;br /&gt;&amp;nbsp; 而且 %{我按下&amp;lt;发送&amp;gt;按钮}&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;Then /^应该有封重设密码的邮件发送至&amp;lt;(.+)&amp;gt;$/ do |email|&lt;br /&gt;&amp;nbsp; user = User.find_by_email(email)&lt;br /&gt;&amp;nbsp; user.reset_password_token.should_not be_blank&lt;br /&gt;&amp;nbsp; sent = ActionMailer::Base.deliveries.last&lt;br /&gt;&amp;nbsp; sent.to.should eql([user.email])&lt;br /&gt;&amp;nbsp; sent.subject.should =~ /重设/&lt;br /&gt;&amp;nbsp; sent.body.should =~ /#{user.reset_password_token}/&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;When /^我访问&amp;lt;(.*)&amp;gt;邮件中重设密码的链接$/ do |email|&lt;br /&gt;&amp;nbsp; user = User.find_by_email(email)&lt;br /&gt;&amp;nbsp; visit edit_user_password_url(user.id, :token =&amp;gt; user.reset_password_token)&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;When /^我更新密码为&amp;lt;(.+)\/(.+)&amp;gt;$/ do |new_password, new_password_confirmation|&lt;br /&gt;&amp;nbsp; 而且 %{我在输入框&amp;lt;user_password&amp;gt;中输入&amp;lt;#{new_password}&amp;gt;}&lt;br /&gt;&amp;nbsp; 而且 %{我在输入框&amp;lt;user_password_confirmation&amp;gt;中输入&amp;lt;#{new_password_confirmation}&amp;gt;}&lt;br /&gt;&amp;nbsp; 而且 %{我按下&amp;lt;更新密码&amp;gt;按钮}&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存user_steps.rb。运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/reset_password.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SdRpC-FZ2JI/AAAAAAAAAdI/TE9ce8ooOsU/user_demo_error_43.png" /&gt;&lt;br /&gt;&lt;br /&gt;测试结果告诉我们接下来应该该做些什么；首先，需要让测试程序能够导向找回密码的页面，这个页面及其相关的路由配置尚无；还有未定义reset_password_token等等。任务不少，我们先从比较简单的找回密码页面开始。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 配置路由 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/support/paths.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;修改 path_to 方法，续添一条找回密码页面的访问路径，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;def path_to(page_name)&lt;br /&gt;&amp;nbsp; case page_name&lt;br /&gt;&amp;nbsp; &lt;br /&gt;&amp;nbsp; when /首页/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; root_path&lt;br /&gt;&amp;nbsp; when /用户注册页面/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; signup_path&lt;br /&gt;&amp;nbsp; when /用户登录页面/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; login_path&lt;br /&gt;&lt;font color="#666666"&gt;&amp;nbsp; when /找回密码的页面/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; new_password_path&lt;/font&gt;&lt;br /&gt;&amp;nbsp; &lt;br /&gt;&amp;nbsp; # Add more page name =&amp;gt; path mappings here&lt;br /&gt;&amp;nbsp; &lt;br /&gt;&amp;nbsp; else&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise "Can't find mapping from \"#{page_name}\" to a path."&lt;br /&gt;&amp;nbsp; end&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存paths.rb。继续在config/routes.rb文件中定义new_password_path，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit config/routes.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;修改map.resources如下，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; map.resources :users, :has_one =&amp;gt; :password&lt;br /&gt;&amp;nbsp; map.resources :sessions,:passwords&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;注意，passwords资源是一组嵌套路由，隶属于users资源；如果读者朋友们对 Restful 不陌生，想必一定很好理解。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 编写找回密码的业务流程 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;$ ruby script/generate controller Passwords new create edit update&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;new 方法用来渲染找回密码页面；&lt;br /&gt;create 方法用来发送重设密码的邮件；&lt;br /&gt;edit 方法用来渲染修改密码页面；&lt;br /&gt;update 方法更新用户密码。&lt;br /&gt;&lt;br /&gt;修改找回密码页面，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/views/passwords/new.html.erb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;lt;h1&amp;gt;找回密码&amp;lt;/h1&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;p&amp;gt;请输入您注册时使用的电子邮箱，系统会发送一封关于重设密码的邮件到您的邮箱中。&amp;lt;/p&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;% form_tag passwords_path do %&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= label_tag 'email', '电子邮箱' %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= text_field_tag 'email' %&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= submit_tag '发送' %&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;lt;% end %&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存app/views/passwords/new.html.erb。修改PasswordsController，在create方法中添加一些业务逻辑，&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;&lt;br /&gt;$ gedit app/controllers/passwords_controller.rb&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; def create&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; user = User.find_by_email(params[:email])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if user.nil?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash.now[:notice] = "没有找到#{params[:email]}这个用户"&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; render :action =&amp;gt; :new&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; else&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user.forgot_password!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash[:notice] = "邮件发送成功！请注意查收。"&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; redirect_to login_path &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存app/controllers/passwords_controller.rb。运行测试，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;$ ruby script/cucumber -l zh-CN features/reset_password.feature&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SdRqA1RKcgI/AAAAAAAAAdU/IBgYzJFjwnE/user_demo_error_44.png" /&gt;&lt;br /&gt;&lt;br /&gt;没有找到User实例对象的forgot_password!方法，因为我们还没有在UserModel中定义该方法；该方法在用户忘记密码提交Email后会生成一份用于重设密码链接的标识码。标识码不仅是构成密码重设链接的一部分，而且还将存入数据库。当用户访问密码重设链接的时候，系统会校验数据库里边的标识码和链接中的标识码是否一致，如果一致则呈现给用户重设密码页面。&lt;br /&gt;&lt;br /&gt;下面我们修改UserModel添加这个forgot_password!方法。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; # 生成忘记密码后的标识码&lt;br /&gt;&amp;nbsp; # 且标识码在24小时后失效&lt;br /&gt;&amp;nbsp; def forgot_password!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.reset_password_token = generate_token&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.reset_password_token_expires_at = 24.hours.from_now&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; save(false)&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;我们知道在Ruby的类中，self调用的方法是类方法，self调用的属性是已定义过的实例变量。很明显，上述的forgot_password!方法中self调用的明显是实例变量。要在UserModel类中定义这两个实例变量非常简单，Rails的ActiveRecord会自动将模型类所对应的数据表的字段注册为该模型类的实例变量，基于AR带给我们开发者的一些便利，我们给users表新增这两个字段即可；还记得前面我们说过要将标识码的相关信息存入数据库的这一初衷吗？&lt;br /&gt;&lt;br /&gt;下面我们给users新增两个字段，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/generate migration ForgotPassword&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&lt;br /&gt;$ gedit db/migrate/*_forgot_password.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;class ForgotPassword &amp;lt; ActiveRecord::Migration&lt;br /&gt;&amp;nbsp; def self.up&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; add_column :users, :reset_password_token, :string&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; add_column :users, :reset_password_token_expires_at, :datetime&lt;br /&gt;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp; def self.down&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; remove_column :users, :reset_password_token_expires_at&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; remove_column :users, :reset_password_token&lt;br /&gt;&amp;nbsp; end&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ rake db:migrate&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&lt;br /&gt;$ rake db:test:prepare&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;运行测试看看，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/reset_password.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SdRqhb2Lf-I/AAAAAAAAAdg/unJqVMRw4FY/user_demo_error_45.png" /&gt;&lt;br /&gt;&lt;br /&gt;发送关于密码重设的邮件这里出了点问题，事实上这时候邮件根本就还没有发送，因为我们没有编写关于发送密码重设邮件的代码，这可不是一个小的疏忽，得补上。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user_mailer.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;添加如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; def forgot_password(user, sent_at = Time.now)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; subject&amp;nbsp;&amp;nbsp;&amp;nbsp; '请重设您的密码'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; recipients user.email&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; from&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 'Admin'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; sent_on&amp;nbsp;&amp;nbsp;&amp;nbsp; sent_at&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; body&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :username =&amp;gt; user.username,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :url&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; edit_user_password_url(user.id, :token =&amp;gt; user.reset_password_token)&lt;br /&gt;&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/views/user_mailer/forgot_password.text.html.erb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;亲爱的 &amp;lt;%=@username%&amp;gt;：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 请点击下面的链接找回您的密码：&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%=link_to @url, @url%&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;修改 UserObserver，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user_observer.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;添加如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; def after_save(user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; UserMailer.deliver_forgot_password(user) if user.forgot_password&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&lt;br /&gt;$ gedit app/models/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;添加，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;attr_reader :forgot_password&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;修改 forgot_password! 方法，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; def forgot_password!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @forgot_password = true&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.reset_password_token = generate_token&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.reset_password_token_expires_at = 24.hours.from_now&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; save(false)&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存 app/models/user.rb。运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/reset_password.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SdRq449WwJI/AAAAAAAAAds/jR2WwC7z5xg/user_demo_error_46.png" /&gt;&lt;br /&gt;&lt;br /&gt;Could not find field: "user_password" (Webrat::NotFoundError)&lt;br /&gt;&lt;br /&gt;抛出这一错误不奇怪呃，这个“user_password“表单字段是属于重设密码页面的。到此为止系统已经可以发送一封关于重设密码的邮件到用户的注册邮箱中；可是我们目前还没有编写重设密码的页面及其业务逻辑，我们来打理接下来的工作。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/views/passwords/edit.html.erb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;lt;h1&amp;gt;重设密码&amp;lt;/h1&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;%= error_messages_for :user %&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;lt;% form_for(:user,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :url =&amp;gt; user_password_path(@user, :token =&amp;gt; @user.reset_password_token),&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :html =&amp;gt; { :method =&amp;gt; :put }) do |form| %&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= form.label :password, '新密码' %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= form.password_field :password %&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= form.label :password_confirmation, '确认新密码' %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= form.password_field :password_confirmation %&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;%= form.submit '更新密码', :disable_with =&amp;gt; '正在更新，请稍后...' %&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;lt;% end %&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存模板文件。修改 PasswordsController，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/controllers/passwords_controller.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; def edit&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.find_by_id_and_reset_password_token(params[:user_id], &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; params[:token])&lt;br /&gt;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp; def update&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.find_by_id_and_reset_password_token(params[:user_id], &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; params[:token])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if @user.update_password(params[:user][:password], &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; params[:user][:password_confirmation])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.email_confirm! unless @user.activated?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; sign_user_in(@user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash[:notice] = "密码更新成功！"&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; redirect_to user_path(@user.id)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; else&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash.now[:notice] = "密码更新失败！"&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; render :action =&amp;gt; :edit&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;添加如上两个action后，保存PasswordsController。接着修改 UserModel，&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&lt;br /&gt;$ gedit app/models/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;在protected之前添加如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; # 更新密码&lt;br /&gt;&amp;nbsp; def update_password(new_password, new_password_confirmation)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; = new_password&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.password_confirmation = new_password_confirmation&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if valid?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.reset_password_token&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; = nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.reset_password_token_expires_at = nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; save&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存app/models/user.rb。运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/reset_password.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SdRrRnitIKI/AAAAAAAAAd4/AdADMhXYrns/user_demo_error_47.png" /&gt;&lt;br /&gt;&lt;br /&gt;当我退出网站，而且我以&amp;lt;xuliicom@gmail.com/newpassword&amp;gt;这个身份登录，那么我应该成功登录网站。这原本是我们之前在最后一个故事场景里写好的预期，但根据上面测试结果可以明显地看出我们用新密码登录失败，这是怎么回事呢？&lt;br /&gt;&lt;br /&gt;最容易想到的可能就是密码没有成功被更新；尽管测试结果告诉我们，已经看到&amp;lt;密码更新成功&amp;gt;的提示信息了，根据我们前面学习到的经验，users表记录中加密过的密码不一定真正地变更过……&lt;br /&gt;&lt;br /&gt;也许可以经过两次手工测试证明物理数据未被更新这一假设，在此之前，我们还算根据失败的测试结果找找程序上的原因。既然我们假设users表中的密文没有更新，不妨从UserModel开始；当编辑器的滚动条在文件app/models/user.rb中上下滑行的一小会儿时间里，我们惊奇地发现问题大概是处在钩子方法那里。下面是一小段源码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; # 钩子方法，保存之前生成 password_salt&lt;br /&gt;&amp;nbsp; # 并使用 password_salt 和原始密码来加密生成新密码&lt;br /&gt;&amp;nbsp; before_create :initialize_salt, :encrypt_password, :initialize_activation_token&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;现在你应该看出是什么问题了？问题出在这个时候就不应该用before_create，而应该是before_save；前者会在数据模型创建新记录之前调用参数中指定的方法；后者则在新建记录或者更新旧有记录时都会执行回调。在这个应用程序中，新用户注册需要生成密文，这会创建新记录；已经注册过的用户更新密码同样需要经过加密处理，这一操作会更新旧有记录；无论是新建还是更新记录，它们彼此都有一个共同操作，就是将数据保存到数据库这一过程，在这个关口上执行回调处理，也只有before_save能担此大任。&lt;br /&gt;&lt;br /&gt;我们将上面代码中的before_create修改为&lt;font color="#999999"&gt;before_save&lt;/font&gt;，然后修改initialize_activation_token方法如下，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; # 生成激活码&lt;br /&gt;&amp;nbsp; def initialize_activation_token&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if new_record?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.activation_token = generate_token&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;new_record? 方法用来判断是不是新记录。或者改成下面这样也行，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; before_save :initialize_salt, :encrypt_password, :encrypt_password&lt;br /&gt;&amp;nbsp; before_create :initialize_activation_token&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存app/models/user.rb；运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/reset_password.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SdRsCJFtvGI/AAAAAAAAAeQ/Ts1cHtPtuXQ/user_demo_ok_48.png" /&gt;&lt;br /&gt;&lt;br /&gt;测试通过！hoho~ 最后笔者再重复强调一遍：开发人员最好亲临现场手工测试几次，确保程序真的万无一失。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 小结 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;在这一章里，我们学习到了这些知识：Cucumber的运行是以每个故事用例为单位进行的一次集成测试；每个故事用例之间彼此独立（在测试运行时独立），又彼此有联系（基于已有功能叠加）；故事场景中的情节只在当前故事中有效，无法跨场景访问，如果需要在其他场景中运行，必须在其他场景中重复定义该情节。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;br /&gt;### 下节预告 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;通过这一系列教程：用户注册、邮件激活帐号、用户登录、登录并“记住我”、用户注销退出和找回密码，我们使用Cucumber+Rspec一步一步循序渐进地开发完成了一个基本的用户帐号系统。这个系统，说大不大，说小也不小了，基本上每个开放注册的站点都会有这么一个用户系统；所以，是时候将它作为一个独立的模块release出来了，不过在此之前，重构和测试是迭代开发中必不可少的一步。在接下来的一章里，笔者将会围绕测试重构展开，我们会配置Cucumber以适应测试环境，还有给测试代码添加辅助方法以及使用Factory_girl等细节，敬请关注！&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 提交工作成果到GIT仓库 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;$ git status&lt;br /&gt;$ git add .&lt;br /&gt;$ git commit -m "People can be change their passwords."&lt;br /&gt;$ git checkout master&lt;br /&gt;$ git merge reset_password&lt;br /&gt;$ git branch -d reset_password&lt;br /&gt;$ git tag v6&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;（注意，真正的开发中可不是到功能开发完毕了才commit，而是边开发边add和commit。为了方便演示编码过程，文章中没有一一列举。）&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/3443179681250049771-3861886344550110296?l=l404.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://l404.blogspot.com/feeds/3861886344550110296/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=3443179681250049771&amp;postID=3861886344550110296' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/3861886344550110296'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/3861886344550110296'/><link rel='alternate' type='text/html' href='http://l404.blogspot.com/2009/03/cucumberrspec6.html' title='使用Cucumber+Rspec玩转BDD(6)——找回密码'/><author><name>404</name><uri>http://www.blogger.com/profile/08660239939199305528</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/_JmYLjCQND0c/SXaZ4wfUELI/AAAAAAAAANg/iLlbANMP1F0/S220/8b1111f6-216a-3853-a396-ee4cff0a219c.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://lh6.ggpht.com/_JmYLjCQND0c/SdRov1YeMNI/AAAAAAAAAc8/Ft29r30AuPs/s72-c/user_demo_error_42.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-3443179681250049771.post-6262390683972362004</id><published>2009-03-20T23:30:00.001+08:00</published><updated>2009-04-02T15:59:49.496+08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Cucumber'/><category scheme='http://www.blogger.com/atom/ns#' term='Rspec'/><category scheme='http://www.blogger.com/atom/ns#' term='TDD'/><category scheme='http://www.blogger.com/atom/ns#' term='Rails'/><title type='text'>使用Cucumber+Rspec玩转BDD(5)——安全退出</title><content type='html'>&lt;b&gt;&lt;br /&gt;### 温故知新 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;为了保护用户的隐私，限制特定资料的访问，前面我们给系统增加了登录功能；紧接着，又为了方便用户在一段时间之内不必重复登录操作，我们实现了用户的持久登录状态，即“记住我”功能。如果浏览器未关闭，或者用户一直处于在线状态，而用户自己并没有使用这台设备，很显然，这对用户的帐号是非常危险的；基于此，系统应该提供一个给用户手工注销在线状态退出站点的功能。&lt;br /&gt;&lt;br /&gt;为了获得更好的阅读体验，读者朋友们可以在这里下载源码：&lt;a target="_blank" href="http://github.com/404/bdd_user_demo/tree/master"&gt;http://github.com/404/bdd_user_demo/tree/master&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 新建工作分支 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ git checkout -b user_logout&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 用户注销退出功能 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp; 1.提供一个“退出”链接，用户登录后点击该链接可以注销在线状态；&lt;br /&gt;&amp;nbsp; 2.用户登录并勾选记住我后，点击“退出”链接可以注销在线状态，下次访问的时候将不再自动登录。&lt;br /&gt;&lt;br /&gt;下面来编写对应于以上功能的故事场景。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 故事用例之用户注销退出 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/user_logout.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;功能: 用户安全退出&lt;br /&gt;&amp;nbsp; 为了保护我的帐号不被他人非法使用&lt;br /&gt;&amp;nbsp; 作为一名已经登录的在线用户&lt;br /&gt;&amp;nbsp; 我希望能够安全退出&lt;br /&gt;&amp;nbsp; &lt;br /&gt;&amp;nbsp; 场景: 用户注销在线状态&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过且已经激活了帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我以&amp;lt;xuliicom@gmail.com/password&amp;gt;这个身份登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该成功登录网站&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我退出网站&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;您已经安全退出&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该尚未登录&lt;br /&gt;&lt;br /&gt;&amp;nbsp; 场景: 用户在持久在线状态下退出&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过且已经激活了帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我以&amp;lt;xuliicom@gmail.com/password&amp;gt;这个身份登录并勾选&amp;lt;记住我&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该成功登录网站&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我退出网站&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;您已经安全退出&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该尚未登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我关闭网页下次再来访问的时候&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该尚未登录&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;可以把一个feature文件当作一份书面需求的电子版，如果你觉得文件开头几句没什么用（为了...作为...我希望...），那直接在那里罗列出功能简要咯，故事场景就当作详尽的需求来写。例如：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;功能: 用户安全退出&lt;br /&gt;&lt;font color="#666666"&gt;&amp;nbsp; 1.提供一个“退出”链接，用户登录后点击该链接可以注销在线状态；&lt;br /&gt;&amp;nbsp; 2.用户登录并勾选记住我后，点击“退出”链接可以注销在线状态，下次访问的时候将不再自动登录。&lt;/font&gt;&lt;/font&gt;&lt;font color="#999999"&gt;&lt;br /&gt;&amp;nbsp; &lt;br /&gt;&amp;nbsp; 场景1&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ...&lt;br /&gt;&lt;br /&gt;&amp;nbsp; 场景2&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ...&lt;br /&gt;&lt;/font&gt;&lt;br /&gt;若真那样，说不定可以节约不少会议时间，因为用于测试的故事文本（feature文件）完全可以替代现实中的功能需求书，而且更加灵活。再向前一步，就可以直接把客户说的需求整理到测试中去，完了发动测试引擎一直在那转动着，开发人员和测试引擎凑到一块儿玩结对编程……敏捷开发，我们还需要文档么？&lt;br /&gt;&lt;br /&gt;回头干正事儿，保存 user_logout.feature。运行测试看看它能告诉我们应该做些什么，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_logout.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SdMGbh-0jeI/AAAAAAAAAbE/lOBrcD5fSTI/user_demo_error_36.png" /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 编写测试脚本 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;如上图所示，我们需要为“当 我退出网站”定义所需的运行脚本。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/step_definitions/user_steps.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;# user logout&lt;br /&gt;&lt;br /&gt;When /^我退出网站$/ do&lt;br /&gt;&amp;nbsp; visit '/logout', :delete&lt;br /&gt;end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存user_steps.rb。运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_logout.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SdMGx926dXI/AAAAAAAAAbc/IRAFbaWnjN8/user_demo_error_37.png" /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 配置登出路由（logout_path） ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;没有找到"/logout"这一访问路径，因为我们还没有在 routes.rb 配置文件中定义"/logout"。修改 routes.rb 文件，添加这条路由信息，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit config/routes.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;为了sessions资源看起来有紧凑的结构，我们稍微变换了login_path的定义，与logout_path整合到了一起。&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; map.with_options :controller =&amp;gt; 'sessions' do |page|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; page.login '/login', :action =&amp;gt; 'new'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; page.logout '/logout', :action =&amp;gt; 'destroy'&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 实现用户注销退出 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;既然 "/logout" 路由指向了 SessionsController 类的 destroy 方法，那么我们还需要编写 destroy 方法的具体实现。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/controllers/sessions_controller.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;添加 destroy 方法，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; def destroy&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; forget(current_user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; reset_session&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash[:notice] = "您已经安全退出！"&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; redirect_to login_path&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;在 private 之后添加 forget 方法，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; def forget(user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; user.forget_me! if user&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; cookies.delete :remember_token&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 删除服务端的remember_token ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;在forget方法中，程序调用了User实例对象的forget_me!方法；然后清除了当前客户端与服务端会话的cookies；仅仅清除客户端的cookies还不够，服务器上的也应该一并删除。&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;$ gedit app/models/user.rb&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;添加forget_me!方法，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp; # 删除数据库里边的remember_token&lt;br /&gt;&amp;nbsp; def forget_me!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.remember_token_expires_at = nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.remember_token&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; = nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; save(false)&lt;br /&gt;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存 user.rb。运行测试，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_logout.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SdMHYhYKBjI/AAAAAAAAAb0/mU1hDOytk2Y/user_demo_ok_38.png" /&gt;&lt;br /&gt;&lt;br /&gt;测试通过！&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 亲临现场 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;为了方便登录用户能够以鼠标点击的方式退出站点，也为了方便开发人员自己手工测试，此时还需要给登录用户提供一个用于注销退出的链接。在之前设置的访问控制中，用户资料显示页面只能在用户登录以后才可见，我们可以将此退出链接加到这个页面中。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/views/users/show.html.erb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;在该模板文件末尾加上如下一段代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp; &amp;lt;%= link_to "安全退出", logout_path %&amp;gt;&lt;br /&gt;&amp;lt;/p&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;保存 show.html.erb。打开 Web Server 手工测试看看，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/server&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;打开浏览器，登录到用户资料查看页面；&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SdMHizc1FoI/AAAAAAAAAcA/BtaDa1UAy-s/user_demo_ok_39.png" /&gt;&lt;br /&gt;&lt;br /&gt;点击“安全退出”链接，我们看到系统将我们带到了用户登录页面；&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SdMHuObAQ4I/AAAAAAAAAcM/gaSI7GpueQQ/user_demo_ok_40.png" /&gt;&lt;br /&gt;&lt;br /&gt;再次刷新刚才那个用户资料显示页面，系统给我们呈现的是一张登录表单；事实说明我们已经成功登出了。&lt;br /&gt;&lt;br /&gt;&lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SdMH3-uzyTI/AAAAAAAAAcY/uFMdslv1g_I/user_demo_ok_41.png" /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 下节预告 ###&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;接下来的一章里，我们将会回到发送邮件的操作上，与之前发送激活邮件不同的是，下一次将会给忘记密码的用户发送一封用于找回密码的邮件。所以，我们下一章的主题就是找回密码。&lt;br /&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;b&gt;### 提交工作成果到GIT仓库 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;$ git status&lt;br /&gt;$ git add .&lt;br /&gt;$ git commit -m "A user can be logout."&lt;br /&gt;$ git checkout master&lt;br /&gt;$ git merge user_logout&lt;br /&gt;$ git branch -d user_logout&lt;br /&gt;$ git tag v5&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;（注意，真正的开发中可不是到功能开发完毕了才commit，而是边开发边add和commit。为了方便演示编码过程，文章中没有一一列举。）&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/3443179681250049771-6262390683972362004?l=l404.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://l404.blogspot.com/feeds/6262390683972362004/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=3443179681250049771&amp;postID=6262390683972362004' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/6262390683972362004'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/6262390683972362004'/><link rel='alternate' type='text/html' href='http://l404.blogspot.com/2009/03/cucumberrspec5.html' title='使用Cucumber+Rspec玩转BDD(5)——安全退出'/><author><name>404</name><uri>http://www.blogger.com/profile/08660239939199305528</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/_JmYLjCQND0c/SXaZ4wfUELI/AAAAAAAAANg/iLlbANMP1F0/S220/8b1111f6-216a-3853-a396-ee4cff0a219c.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://lh6.ggpht.com/_JmYLjCQND0c/SdMGbh-0jeI/AAAAAAAAAbE/lOBrcD5fSTI/s72-c/user_demo_error_36.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-3443179681250049771.post-1088630443051901264</id><published>2009-03-13T12:41:00.005+08:00</published><updated>2009-04-02T15:59:33.008+08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Cucumber'/><category scheme='http://www.blogger.com/atom/ns#' term='Rspec'/><category scheme='http://www.blogger.com/atom/ns#' term='TDD'/><category scheme='http://www.blogger.com/atom/ns#' term='Rails'/><title type='text'>使用Cucumber+Rspec玩转BDD(4)——用户登录并“记住我”</title><content type='html'>&lt;b&gt;&lt;br /&gt;### 温故知新 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在&lt;a href="http://l404.blogspot.com/2009/03/cucumberrspec3_6419.html"&gt;上一篇文章&lt;/a&gt;中，我们参照文章内容完成了用户登录功能的开发工作。此时，注册用户可以顺利登录站点，查看用户资料等等；但这一状态也只限于当前的浏览器窗口，如果浏览器关闭了，用户重新打开浏览器下次访问的时候，还是需要来到登录页面进行重新登录。若不是做交易支付型站点，为了追求好一点的用户体验，我们可以给用户预留一个可选项；用户在登录的时候可以勾选“记住我”，一段时间内用户将不必重新登录。要实现用户的这种持久登录状态，我们应该怎么做呢？不妨来了解我们接下来的活儿。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 为了获得更好的阅读体验，读者朋友们可以在这里下载源码：&lt;a target="_blank" href="http://github.com/404/bdd_user_demo/tree/master"&gt;http://github.com/404/bdd_user_demo/tree/master&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 新建工作分支 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ git checkout -b remember_me&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在有效时间内，要保持用户的在线状态。那么第一个问题会是，我们得知道用户是否已经登录呢？按照我们之前的预期，比如用户的资料应该是受保护的，只有当用户登录以后才可以查看；那就有必要在程序上做一些访问控制。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 实现简单的访问控制 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 使用 Rails的 before_filter 钩子方法可以非常方便地实现我们的目的。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/controllers/users_controller.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在 UserController 类中的任何方法之前加上如下一段代码：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;before_filter :login_required, :only =&amp;gt; [:show]&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这样在查看用户资料的时候会检查用户是否已经登录，如果未登录会提供一张登录用的表单，如果已经登录了就会呈现用户资料。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们继续秉承测试先行这一理念，从编写“用户登录且勾选记住我”的故事开始。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 用户登录之“记住我” ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在文件尾部续添如下文本：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户已激活帐号且使用有效身份登录并勾选记住我&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过且已经激活了帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我以&amp;lt;xuliicom@gmail.com/password&amp;gt;这个身份登录并勾选&amp;lt;记住我&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;登录成功&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该成功登录网站&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我关闭网页下次再来访问的时候&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该依然保持登录状态&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 运行测试，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_login.feature&lt;br /&gt;&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SdGc5nBc29I/AAAAAAAAAW4/XGPxexxU0D0/user_demo_error_31.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试失败，根据提示信息来看，我们需要添加一些故事情节运行所需的测试脚本。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 添加用户驱动故事运行的测试脚本 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/step_definitions/user_steps.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 续添如下脚本：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; When /^我关闭网页下次再来访问的时候$/ do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 %{session已经被清除}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 %{我来到用户登录页面}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; When /^session已经被清除$/ do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; request.reset_session&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; request.session[:user_id].should be_nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Then /^我应该依然保持登录状态$/ do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 很遗憾，在测试代码中，cookies里边放符号索引会返回nil对象&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 用字符串来索引没问题&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 更多信息可以查阅 http://dev.rubyonrails.org/ticket/5924&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; cookies['remember_token'].should_not be_blank&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; request.session[:user_id].should_not be_nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 修改 “When /^我以&amp;lt;(.+)\/(.+)&amp;gt;这个身份登录$/ do ... end” 这段代码如下：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; When /^我以&amp;lt;(.+)\/(.+)&amp;gt;这个身份登录(并勾选&amp;lt;记住我&amp;gt;)?$/ do |username_or_email, password, remember|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 %{我来到用户登录页面}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 %{我在输入框&amp;lt;用户名或邮箱&amp;gt;中输入&amp;lt;#{username_or_email}&amp;gt;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 %{我在输入框&amp;lt;密码&amp;gt;中输入&amp;lt;#{password}&amp;gt;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 %{我勾选&amp;lt;记住我&amp;gt;} if remember&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 %{我按下&amp;lt;登录&amp;gt;按钮}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 为 “当 我勾选&amp;lt;记住我&amp;gt;” 添加对应的运行脚本，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; When /^我勾选&amp;lt;(.+)&amp;gt;$/ do |field|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; check(field) &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存 user_steps.rb。运行测试，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SdGdOw4N1zI/AAAAAAAAAXE/5RLwZRoC5io/user_demo_error_32.png" /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 观测试了解工作内容 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试结果返回 “Could not find field: "记住我" (Webrat::NotFoundError)”的相关信息，提示没有找到关于“记住我”的这个表单域，不用多想，这个“记住我”的多选框应该出现在用户登录页面。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ gedit app/views/sessions/new.html.erb&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 修改后的登录页面代码如下，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% form_tag sessions_path do %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= label_tag 'username_or_email', '用户名或邮箱' %&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= text_field_tag 'username_or_email' %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= label_tag 'password', '密码' %&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= password_field_tag 'password' %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= check_box_tag 'remember_me', 1, true %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= label_tag 'remember_me', '记住我' %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= submit_tag '登录' %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% end %&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存 app/views/sessions/new.html.erb。运行测试，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SdGdfyno6tI/AAAAAAAAAXQ/V5QtFu6nHxY/user_demo_error_33.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 问题出在 user_steps.rb 文件的第86行，我们来看看这行代码的内容，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SdGdpv6N-uI/AAAAAAAAAXc/cIO8CLmhCfE/user_demo_ok_34.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试代码里边写明了，如果用户关闭网页再次访问的时候，cookies[:remember_token]的值应该不为空，这样就可以实现记住我的功能。测试结果告诉我们该值为空，为了达到我们想要的效果，我们来做些实际的编码工作。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 之前在用户登录页面的模板文件中，我们已经添加了供用户可选“记住我”的选项，下面添加一些处理业务流程的代码。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/controllers/sessions_controller.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 修改 create 方法，在 sign_user_in(@user)之前添加如下一句代码，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;remember(@user) if remember?&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这样用户在勾选“记住我”选项之后，系统会自动设置记住用户的相关细节，不过这些具体细节还需要我们通过编码来完成。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 继续在 SessionsController 中编写刚才那段代码中用到的两个方法，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; private&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def remember?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; params[:remember_me] &amp;amp;&amp;amp; params[:remember_me] == "1"&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def remember(user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user.remember_me!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; cookies[:remember_token] = {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :value&amp;nbsp;&amp;nbsp; =&amp;gt; user.remember_token,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :expires =&amp;gt; user.remember_token_expires_at &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们将这两个方法设为私有仅供程序内部使用，上面的 remember 方法已经涉及到数据存取；我们还需要修改 User 模型类以衔接上述的业务逻辑。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 首先增添上述代码用到的两个数据字段，即 remember_token 和 remember_token_expires_at；之所以添加这两个字段，是因为服务端需要记录客户端自动登录且唯一的cookie标识，并用该标识来验证客户端的请求是否有效，如果有效就可以打破HTTP协议无状态的限制建立持久的会话连接，如果客户端的cookie headers是伪造或失效的，那么很遗憾地非法请求将不得逞，并导向用户登录页面，提示该用户登录。下面我们来添加数据迁移文件，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ ruby script/generate migration RememberMe&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ gedit db/migrate/*_remember_me.rb&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; class RememberMe &amp;lt; ActiveRecord::Migration&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def self.up&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; add_column :users, :remember_token, :string&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; add_column :users, :remember_token_expires_at, :datetime&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def self.down&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; remove_column :users, :remember_token_expires_at&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; remove_column :users, :remember_token&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存 db/migrate/*_remember_me.rb。然后执行迁移，&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ rake db:migrate&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ rake db:test:prepare&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 接着修改 UserModel，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 添加如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # remember_token 是否失效&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def remember?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; remember_token_expires_at &amp;amp;&amp;amp; Time.now &amp;lt; remember_token_expires_at&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 记住多长时间&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def remember_me!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; remember_me_until 2.weeks.from_now&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 保存“记住我”的相关设置&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def remember_me_until(time)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.remember_token_expires_at = time&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.remember_token&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; = encrypt(time)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; save(false)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存 UserModel ，最后在 ApplicationController 类中添加一个读取并校验 cookie 的方法。用户访问的时候检查cookie，如果该cookie有效就可以直接登录了。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/controllers/application.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在 user_from_session 方法之后添加如下方法，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def user_from_cookie&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if cookies[:remember_token]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user = User.find_by_remember_token(cookies[:remember_token])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user &amp;amp;&amp;amp; user.remember? ? user : nil&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 然后修改 current_user 方法，可以接受用户使用 cookie 的方式登录。&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def current_user&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @_current_user ||= (user_from_session || user_from_cookie)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存 ApplicationController，运行测试看看；&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SdGeQBIS_gI/AAAAAAAAAXo/fAb7RcBJTLU/s512/user_demo_ok_35.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试通过!:)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 小结 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这篇文章介绍的内容不是很多，功能也不算复杂。在用户已经能够登录站点的基础上，我们给登录用户加上了“记住我”的功能，这是一种持久登录状态。说到持久一词，就不得不提到HTTP协议是无状态的，无状态在这里意指在一般的B/S连接中，Server 无法识别特定的 Browser，因为一台 Web Server 响应的 Browsers 不计其数，Server 没办法知道当前所响应的Browser是谁，也不记得这Browser之前是否请求过。不过有了 cookie，就可以打破HTTP协议无状态的这一限制。cookie是一种在客户端存储数据并以此来跟踪和识别用户的机制。Server 响应 Browser 的请求时会发送一个带 set-cookie 的 http headers，Browser 会在本地记住这一cookies 数据；当 Browser 再次请求时，就会将这一 cookies 发送给 Server ，Server 在响应 Browser 请求的同时接受并读取此 cookies，以此来达到跟踪和识别用户的目的。如果服务端没有特别地设置cookies，客户端针对这一站点的cookies将随浏览器进程的关闭而失效。在之前我们使用session来做用户登录即是如此，因为Rails将会话数据（session data）存在客户端的cookies里边，cookies的有效期是随浏览器的关闭而失效的，所以当用户关闭浏览器后重新打开再次访问就需要登录；后来我们在程序里显示地配置了cookies的有效期为两周，当用户第一次登录后，cookies会在用户浏览器中保存两周，那么用户在这两周内就不需要重新登录了。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 相关阅读 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; cookie机制的纯JavaScript实现：&lt;a target="_blank" href="http://chinaonrails.com/q/cookie"&gt;http://chinaonrails.com/topic/view/1449.html&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; php系统和ror系统的用户登录授权问题：&lt;a target="_blank" href="http://chinaonrails.com/q/cookie"&gt;http://chinaonrails.com/topic/view/1711.html&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 了解关于cookie和Rails交互的更多信息：&lt;a target="_blank" href="http://chinaonrails.com/q/cookie"&gt;http://chinaonrails.com/q/cookie &lt;br /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 下节预告 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 接下来的一章里会向读者朋友们演示登录用户如何安全退出，敬请期待！&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 提交工作成果到GIT仓库 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git status&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git add .&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git commit -m "A user can be login with remember me."&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git checkout master&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git merge remember_me&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git branch -d remember_me&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git tag v4&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; （注意，真正的开发中可不是到功能开发完毕了才commit，而是边开发边add和commit。为了方便演示编码过程，文章中没有一一列举。）&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/3443179681250049771-1088630443051901264?l=l404.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://l404.blogspot.com/feeds/1088630443051901264/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=3443179681250049771&amp;postID=1088630443051901264' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/1088630443051901264'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/1088630443051901264'/><link rel='alternate' type='text/html' href='http://l404.blogspot.com/2009/03/cucumberrspec4.html' title='使用Cucumber+Rspec玩转BDD(4)——用户登录并“记住我”'/><author><name>404</name><uri>http://www.blogger.com/profile/08660239939199305528</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/_JmYLjCQND0c/SXaZ4wfUELI/AAAAAAAAANg/iLlbANMP1F0/S220/8b1111f6-216a-3853-a396-ee4cff0a219c.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://lh4.ggpht.com/_JmYLjCQND0c/SdGc5nBc29I/AAAAAAAAAW4/XGPxexxU0D0/s72-c/user_demo_error_31.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-3443179681250049771.post-1420496944018366056</id><published>2009-03-07T23:22:00.005+08:00</published><updated>2009-04-02T15:59:18.003+08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Cucumber'/><category scheme='http://www.blogger.com/atom/ns#' term='Rspec'/><category scheme='http://www.blogger.com/atom/ns#' term='TDD'/><category scheme='http://www.blogger.com/atom/ns#' term='Rails'/><title type='text'>使用Cucumber+Rspec玩转BDD(3)——用户登录</title><content type='html'>&lt;b&gt;&lt;br /&gt;### 温故知新 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在前面的两篇文章中，笔者向读者朋友们分别演示了&lt;a href="http://l404.blogspot.com/2009/02/cucumberrspec-1.html"&gt;用户注册&lt;/a&gt;和&lt;a href="http://l404.blogspot.com/2009/03/cucumberrspec-2.html"&gt;注册用户通过邮件激活帐号&lt;/a&gt;的开发过程。当用户注册成功并激活帐号后，系统应该可以让用户登录站点，这就是我们接下来的活儿。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 为了获得更好的阅读体验，读者朋友们可以在这里下载源码：&lt;a target="_blank" href="http://github.com/404/bdd_user_demo/tree/master"&gt;http://github.com/404/bdd_user_demo/tree/master&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 新建工作分支 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ git checkout -b user_login&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 用户登录功能 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1. 提供一张表单，方便用户输入帐号和密码，帐号可以是用户名或邮箱；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2. 如果用户尚未注册，那么提示登录失败；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 3. 如果用户已注册但未激活，那么系统往该用户的邮箱发送一封激活邮件，并提示用户登录失败，返回登录页面；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 4. 如果用户输入的帐号或密码错误，那么提示用户登录失败，并返回登录页面；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 5. 如果用户输入的帐号和密码正确，那么提示用户登录成功，并将该用户ID记录在session中；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 6. 如果用户输入的帐号和密码正确，且勾选了“记住我”选项，那么提示用户登录成功，并将该用户记录在session和cookie中，然后转向用户登录之前或系统默认的页面。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;br /&gt;### 用户登录之“用户尚未注册” ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们简单地罗列了用户登录功能，随后我们将之转化成故事描述以供测试用，每个功能至少对应一个故事情节。我们先从“用户尚未注册”这个功能开始，下面我们编写用于这个功能的故事场景：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;$ gedit features/user_login.feature&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 功能: 用户登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 为了能够浏览网站只对在线会员可见的那些内容&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 作为一名访客&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我希望能够登录&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户登录功能&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 没有&amp;lt;somebody@somedomain.com&amp;gt;这个用户&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我以&amp;lt;somebody@somedomain.com/password&amp;gt;这个身份登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;用户名或密码错误&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该尚未登录&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 编写用于故事运行的测试代码 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/step_definitions/user_steps.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 添加如下代码：&lt;br /&gt;&lt;font color="#999999"&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Given /^没有&amp;lt;(.*)&amp;gt;这个用户$/ do |email|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; User.find_by_email(email).should be_nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; When /^我以&amp;lt;(.+)\/(.+)&amp;gt;这个身份登录$/ do |username_or_email, password|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 %{我来到用户登录页面}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 %{我在输入框&amp;lt;&lt;/font&gt;&lt;font color="#999999"&gt;用户名或邮箱&lt;/font&gt;&lt;font color="#999999"&gt;&amp;gt;中输入&amp;lt;#{username_or_email}&amp;gt;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 %{我在输入框&amp;lt;&lt;/font&gt;&lt;font color="#999999"&gt;密码&lt;/font&gt;&lt;font color="#999999"&gt;&amp;gt;中输入&amp;lt;#{password}&amp;gt;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 %{我按下&amp;lt;登录&amp;gt;按钮}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Then /^我应该尚未登录$/ do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; request.session[:user_id].should be_nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 运行测试，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/Sc-F006BlhI/AAAAAAAAAVw/ZB63RxZV9H8/user_demo_error_25.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; “Can't find mapping from "登录页面" to a path. (RuntimeError)”，测试程序未能导向登录页面。还记得前面我们提到过的 paths.rb 文件吗？我们可以在该文件的 path_to 方法中指明登录页面的访问路径。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 配置路由——login_path ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/support/paths.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 修改后的 path_to 方法如下，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def path_to(page_name)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; case page_name&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; when /首页/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; root_path&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; when /用户注册页面/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; signup_path&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#666666"&gt;when /用户登录页面/&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#666666"&gt;login_path&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Add more page name =&amp;gt; path mappings here&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; else&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise "Can't find mapping from \"#{page_name}\" to a path."&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 接下来还需要配置路由来定义login_path，不过在此之前我们需要创建一个用于映射login_path的控制器，通常情况下，这样的控制器的会是SessionController。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/generate rspec_controller Sessions&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在这里，SessionController 的存在是我们定义 login_path 的基础，下面我们来定义登录页面的访问路径。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit config/routes.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ActionController::Routing::Routes.draw do |map|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; map.with_options :controller =&amp;gt; 'users' do |page|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; page.signup '/signup', :action =&amp;gt; 'new'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; page.activate '/activate/:token', :action =&amp;gt; 'activate'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#666666"&gt;map.login '/login', :controller =&amp;gt; 'sessions', :action =&amp;gt; 'new'&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; map.resources :users&lt;font color="#666666"&gt;, :sessions&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; login_path定义完毕，如果你不知道接下来要做什么，此时您可以运行测试看看。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 创建用于用户登录的表单页面 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/views/sessions/new.html.erb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 填充如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% form_tag sessions_path do %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= label_tag 'username_or_email', '用户名或邮箱' %&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= text_field_tag 'username_or_email' %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= label_tag 'password', '密码' %&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= password_field_tag 'password' %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= submit_tag '登录' %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% end %&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 登录验证——用户输入的密码是否正确 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在 user.rb 中添加如下两个方法（写在protected之前）：&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 验证指定帐号的密码是否匹配&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def self.authenticate(username_or_email, password)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; account = username_or_email.to_s&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user = find(:first, :conditions =&amp;gt; ['username = ? or email = ?', account, account])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user &amp;amp;&amp;amp; user.authenticated?(password) ? user : nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 密码是否有效&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def authenticated?(password)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; encrypted_password == encrypt(password)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 数据模型和路由准备就位（这句话大概就是说，我们已经设计好用户该怎么来到注册页面，也告诉后端的数据模型该如何处理数据），下面来编写用于用户登录的业务流程。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 处理用户登录的业务流程 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/controllers/sessions_controller.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 填充如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; class SessionsController &amp;lt; ApplicationController&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def new &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def create&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.authenticate(params[:username_or_email], params[:password])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if @user.nil?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash.now[:notice] = '登录失败，用户名或密码错误！'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; render :action =&amp;gt; :new, :status =&amp;gt; :unauthorized&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; else&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if @user.activated?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; sign_user_in(@user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash[:notice] = '登录成功！'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; redirect_back_or user_path(@user.id)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; else&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; UserMailer.deliver_confirm(@user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; deny_access("登录失败！您的帐号尚未激活，系统已经向您的邮箱重新发送了一封激活邮件，请注意查收！")&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果您又迷惑了，不妨就此打住运行测试看看，测试结果会告诉你该做什么。不过有时候你大脑里或许非常清楚下一步该做什么。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 下一步要做的就是增加访问控制，明确用户登录的意义；如果用户还没有登录，这时候是游客身份，是不可以查看用户资料的，如果登录成功，才可以查看。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 添加访问控制 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/controllers/application.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在 ApplicationController 中添加如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def login_required&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; deny_access unless signed_in?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def signed_in?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; !current_user.nil?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def current_user&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @_current_user ||= user_from_session&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def user_from_session&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if session[:user_id]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user = User.find_by_id(session[:user_id])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user &amp;amp;&amp;amp; user.activated? ? user : nil&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def sign_user_in(user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; sign_in(user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def sign_in(user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if user&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; session[:user_id] = user.id&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def redirect_back_or(default)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; session[:return_to] ||= params[:return_to]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if session[:return_to] &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; redirect_to(session[:return_to])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; else &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; redirect_to(default)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; session[:return_to] = nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def store_location&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; session[:return_to] = request.request_uri if request.get?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def deny_access(flash_message = nil, opts = {})&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; store_location&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash.now[:failure] = flash_message if flash_message&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; render :template =&amp;gt; "/sessions/new", :status =&amp;gt; :unauthorized &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这下加了不少代码，得花点时间看看，看明白了就保存application.rb。接着运行测试，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh3.ggpht.com/_JmYLjCQND0c/Sc-G96ngPVI/AAAAAAAAAV8/3NwcI1lKZ78/user_demo_ok_26.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试通过，未注册用户登录失败，这正符合我们的要求；我们再来看看用户已注册但未激活帐号的情况下能否登录成功。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 用户登录之“用户已注册但尚未激活帐号” ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 正常情况下，用户已注册但尚未激活帐号的情况下，是不能登录成功的。原因很简单，没激活帐号的用户暂且算不上真正的注册用户。下面来试试看，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 添加如下场景：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户已注册但尚未激活帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我以&amp;lt;xuliicom@gmail.com/password&amp;gt;这个身份登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;帐号尚未激活&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该尚未登录&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存。再次运行测试，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/Sc-HOm8bR7I/AAAAAAAAAWI/1eg8ZD-mt84/user_demo_error_27.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试失败，没有在输出中匹配到“帐号尚未激活”这段文本。在前面的编码中，我们一直使用 flash 传递提示信息，而且我们将呈现 flash 中信息的代码写在了 app/views/layouts/application.html.erb 这个模板中。打开这个文件，找到了这段代码：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% if flash[:notice] -%&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p style="color: green"&amp;gt;&amp;lt;%= flash[:notice] %&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% end -%&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 还记得我们在ApplicationController类中定义的deny_access方法吗？我们在那里给 flash[:failure] 赋过值，而layout只针对 flash[:notice] 做了处理，看来我们需要修改这个layout模板的代码。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/views/layouts/application.html.erb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们将&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% if flash[:notice] -%&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p style="color: green"&amp;gt;&amp;lt;%= flash[:notice] %&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% end -%&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这段代码替换为&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;div id="flash"&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% flash.each do |key, value| -%&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;div id="flash_&amp;lt;%= key %&amp;gt;"&amp;gt;&amp;lt;%=h value %&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% end %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/div&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存 application.html.erb。再次运行测试，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/Sc-He7ovzMI/AAAAAAAAAWU/EbHDHwfxeiI/user_demo_ok_28.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试通过，两个故事场景都运行成功！在遭遇这样的小小挫折后，增进了我们往后测试的信心！TDD同样是增量开发的，当我们小幅度地尝试，小幅度的迭代，小范围地测试失败之后带来的总是小小成就的欢欣鼓舞。我们何不继续步步为营，稳打稳扎，循序渐进地编写并运行我们的故事尝尝更多的甜头呢？&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 用户登录之“更多情况..” ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 接下来添加其他的几个故事场景，以测试我们编写的用户登录脚本是否达到指定功能，这下步子可能跨得大了点哦！&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 继续在文件尾添加如下文本：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户已激活帐号但密码输入错误&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过且已经激活了帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我以&amp;lt;xuliicom@gmail.com/wrong_password&amp;gt;这个身份登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;用户名或密码错误&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该尚未登录&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户已激活帐号且邮箱和密码输入正确&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过且已经激活了帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我以&amp;lt;xuliicom@gmail.com/password&amp;gt;这个身份登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;登录成功&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该成功登录网站&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户已激活帐号且用户名和密码输入正确&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过且已经激活了帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我以&amp;lt;404/password&amp;gt;这个身份登录&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;登录成功&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我应该成功登录网站&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存。再次运行测试看看，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/Sc-HuwtHJxI/AAAAAAAAAWg/Rk1vKf62qz4/user_demo_error_29.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试失败，反馈的结果告诉我们还需要分别针对“注册且激活”和“成功登录”定义相关的运行脚本。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/step_definitions/user_steps.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 添加如下代码： &lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Then /^我应该成功登录网站$/ do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; request.session[:user_id].should_not be_nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 然后修改 “ Given /^我已经使用&amp;lt;(.*)\/(.*)\/(.*)&amp;gt;注册过$/ do ... end “ 这段已经定义过的脚本，修改后的代码如下：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Given /^我已经使用&amp;lt;(.*)\/(.*)\/(.*)&amp;gt;注册过(且已经激活了帐号)?$/ do |username, email, password, confirm|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @valid_attributes = {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :username&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; username, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :email&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; email, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; password, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password_confirmation =&amp;gt; password&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.create!(@valid_attributes)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if confirm&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.activation_token = nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.save(false)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存user_steps.rb。继续运行测试，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_login.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/Sc-IDKPgcwI/AAAAAAAAAWs/P-CedEIYZ2I/user_demo_ok_30.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; OK，测试成功，可以松口气了！至此，离用户登录功能的开发只剩下最后一步了，也是我们接下来要做的工作，那就是实现用户的持久登录状态。当用户输入帐号和密码并勾选“记住我”登录后，用户下次打开浏览器访问的时候依然保持用户的在线状态，那么我们要知道怎么判断用户是否已经登录呢？且听下回讲解！:)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 小结 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 此次开发过程相当顺利，故事场景较之前稍微多一些，还好我们进行的是小幅度的迭代测试，我们的开发工作才得以循序渐进。所以学到的一个经验就是，当业务流程稍微复杂的时候，就要分而治之了。这和大事化小，小事化无的道理是一样的。cucumber的运行是以每一个故事场景为单位，可以让我们很方便地做到这一点。比如我们每次只编一个故事，然后通过运行测试来驱动我们编码后，可以让这个故事能够顺利地跑起来；完了之后又继续下一个故事……稳打稳扎，步步为营，这相当地保险，在TDD上玩迭代开发，那中间想必也多出几次成就感了。另外，我们还学到一点，如果不知道下一步怎么走，在测试准备就绪的情况，只需要轻轻地发动测试引擎一切又柳暗花明了。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 提交工作成果到GIT仓库 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git status&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git add .&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git commit -m "A user can be login."&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git checkout master&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git merge user_login&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git branch -d user_login&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git tag v3&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; （注意，真正的开发中可不是到功能开发完毕了才commit，而是边开发边add和commit。为了方便演示编码过程，文章中没有一一列举。）&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/3443179681250049771-1420496944018366056?l=l404.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://l404.blogspot.com/feeds/1420496944018366056/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=3443179681250049771&amp;postID=1420496944018366056' title='2 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/1420496944018366056'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/1420496944018366056'/><link rel='alternate' type='text/html' href='http://l404.blogspot.com/2009/03/cucumberrspec3_6419.html' title='使用Cucumber+Rspec玩转BDD(3)——用户登录'/><author><name>404</name><uri>http://www.blogger.com/profile/08660239939199305528</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/_JmYLjCQND0c/SXaZ4wfUELI/AAAAAAAAANg/iLlbANMP1F0/S220/8b1111f6-216a-3853-a396-ee4cff0a219c.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://lh4.ggpht.com/_JmYLjCQND0c/Sc-F006BlhI/AAAAAAAAAVw/ZB63RxZV9H8/s72-c/user_demo_error_25.png' height='72' width='72'/><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-3443179681250049771.post-3384514496238395337</id><published>2009-03-02T21:41:00.012+08:00</published><updated>2009-04-02T15:58:56.659+08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Cucumber'/><category scheme='http://www.blogger.com/atom/ns#' term='Rspec'/><category scheme='http://www.blogger.com/atom/ns#' term='TDD'/><category scheme='http://www.blogger.com/atom/ns#' term='Rails'/><title type='text'>使用Cucumber+Rspec玩转BDD(2)——邮件激活</title><content type='html'>&lt;b&gt;&lt;br /&gt;### 温故知新 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 前面我们已经完成了&lt;a target="_blank" href="http://l404.blogspot.com/2009/02/cucumberrspec-1.html"&gt;新用户注册&lt;/a&gt;功能的开发，为了方便我们后面的开发工作且不扰乱之前的工作成果，我们先将这份源代码归档并做个标记。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 为了获得更好的阅读体验，读者朋友们可以在这里下载源码：&lt;a target="_blank" href="http://github.com/404/bdd_user_demo/tree/master"&gt;http://github.com/404/bdd_user_demo/tree/master&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 提交工作成果到GIT仓库 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ cd ~/code/user_demo&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git init&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git add .&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git commit -m &lt;/font&gt;&lt;/b&gt;&lt;font color="#999999"&gt;"A user can be able to sign up."&lt;/font&gt;&lt;b&gt;&lt;font color="#999999"&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git tag v1&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; “git init” 会在 ~/code/user_demo 目录中初始化版本库；接着 “git add .” 将 user_demo 目录中的所有文件信息编入索引（index files）；然后 “git commit” 命令将根据 index 中的信息将工作内容提交到项目的GIT仓库里边去，-m 选项加上了本次提交的一些说明；最后 “git tag” 给这次提交所生成的版本号标记了一个别名叫 v1。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 其实好习惯是在新建rails-app后就初始化版本库。由于篇幅的关系，笔者才将些许GIT的内容放到这篇文章中。这不，正好派上用场喽！&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在主干（master）上工作是危险的，因为控制的不够好会扰乱版本，这不是我们愿意看到的。为此，GIT允许我们在主干道的基础上建立新的分支（branch），在分支中进行开发工作，这样好控制风险。比如有时候分支中的工作搞得一塌糊涂，开发人员想重来的时候，直接丢掉删除这个分支再新建一个工作分支重新工作就是了，这对项目中的主干完全没有丝毫影响（不会扰乱你上次提交到master中的工作成果），等你在新分支中开发完毕后，再将这个分支中的工作成果归并到主干中就行。GIT的分支告诉我们，丢掉一个烂摊子比收拾一个烂摊子要轻松得多；潜意识里，我们几乎一致认为这对开发人员的大脑是友好的！:)&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 下面我们在主干的基础上为后面邮件激活这个功能的开发新建一个分支。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 新建工作分支 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ git checkout -b email_activation&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 或者，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ git branch email_activation&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ git checkout email_activation&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 查看当前工作所在的分支，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ git branch&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 会返回项目中的所有分支，前面加星*就是当前的工作分支。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 做开发要步步为营，不是吗？git可以很方便地帮我们做到这点。在归档源码后，接着我们新建了一个名为 email_activation 的分支，并将当前的工作状态从master主干切换到email_activation分支中。这里说明下，此时的 email_activation 相当于之前源码（v1）的一份副本，这份副本是我们进行后续开发的基础；后面我们将在用户已经能够注册的基础上进行用户激活帐号的开发工作，只不过在这个基础上所开发的一举一动都会被记录到email_activation分支中。当用户注册成功并能通过邮件激活帐号后，我们就可以将email_activation分支下的工作成果提交且归并到master主干中，从而把邮件激活的功能和用户注册的功能完美的衔接在一起，同时使得项目的版本干净整洁。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果我们在email_activation的分支中的开发工作不尽人意，怎么办呢？如果是一些小小的修改，那非常好办，直接改成你想要的就是了；可如果是大范围地修改后，结果却不是你想要的，有时会萌发重做的想法。下面就来告诉你一些开倒车的技巧：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果新增了文件，需要先用git add添加（这会被编入git的index，但不会提交到git仓库），否则回滚后会遗留下来（这句话好像就是说，等到你重新开发的时候发现要编码的文件已经存在了）。可以用 git status 命令查看都添加或者修改了哪些文件。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果你当前的工作目录（working tree）已经混乱不堪，但是还没有提交，可以使用：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ git reset --hard&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这会丢弃所有的改变，包括去除已经加到git index里边的内容；然后将 working tree 和 index 恢复到上次commit时的状态。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果想回滚到一个指定的版本，就需要指定版本号：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ git reset --hard v1&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; v1 是我们在之前标记过的别名，即上次commit所生产的版本号别名，也可以替换成commit后的版本号，比如 af2d45c... ，版本号是一个唯一的哈希值，每次commit都会生成一个，省去了你找不到版本号的尴尬；基本上，使用git log 命令都能看到版本号。指定版本号的时候不需要写上所有字符，取前5个就可以，反正能说明版本号是唯一的就行了。比如你只有两次提交记录，指定版本号的时候取哈希值的前两个字符又何尝不可呢？&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 还有，记得 --hard 选项要慎重使用，具体的您可以使用 “git reset -h” 命令查阅更多关于撤销修改的详细信息。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果只是想放弃对某一文件的修改，可以使用 checkout 命令。这个命令不单用于分支间的切换，还可以回滚一个指定的文件内容到上次所做的修改，例如：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ git checkout app/models/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这会放弃对user.rb所做的修改，并将user.rb的内容从上一个已提交的版本中更新回来。当然还可以指定回滚到指定版本，例如：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ git checkout v1 app/models/user.rb&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这会将user.rb的内容从已提交的v1所对应的版本中更新回来。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 好了，到此您已经了解了一些实用的GIT知识；是时候步入正题进行我们的开发工作了，我们来了解下工作内容。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### &lt;/b&gt;邮件激活功能 &lt;b&gt;###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1. 用户成功注册成为网站用户；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2. 系统发送一封包含激活链接的邮件到用户注册时填写的邮箱中；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 3. 用户点击邮箱中的激活链来接激活帐号；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 4. 用户帐号激活成功，并给出帐号激活成功的提示消息。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 根据上面的功能需求，我们在前面两个故事的基础上再添两笔。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 故事用例之用户通过邮件激活帐号 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit features/user_signup.feature&lt;br /&gt;&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 修改后的文件内容如下，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 功能: 注册成为网站会员&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 为了能够浏览网站只对在线会员可见的那些内容&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 作为一名访客&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我希望注册成为网站会员&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户填写无效数据并注册&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我来到用户注册页面&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;用户名&amp;gt;中输入&amp;lt;invalid username&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;电子邮箱&amp;gt;中输入&amp;lt;invalid email&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;密码&amp;gt;中输入&amp;lt;password&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;确认密码&amp;gt;中输入&amp;lt;verify password&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我按下&amp;lt;注册&amp;gt;按钮&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;注册失败&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户填写正确的数据并注册&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我来到用户注册页面&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;用户名&amp;gt;中输入&amp;lt;404&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;电子邮箱&amp;gt;中输入&amp;lt;xuliicom@gmail.com&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;密码&amp;gt;中输入&amp;lt;password&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;确认密码&amp;gt;中输入&amp;lt;password&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我按下&amp;lt;注册&amp;gt;按钮&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;注册成功&amp;gt;的提示信息&lt;br /&gt;&lt;font color="#666666"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 应该有封激活帐号的邮件发送至&amp;lt;xuliicom@gmail.com&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户激活帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假如 我已经使用&amp;lt;404/xuliicom@gmail.com/password&amp;gt;注册过&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我访问&amp;lt;xuliicom@gmail.com&amp;gt;邮件中激活帐号的链接&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;帐号激活成功&amp;gt;的提示信息&lt;/font&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们只是在已有的故事上加了个别子句。为了能让故事跑起来，我们还需要针对故事场景中的情节编写相应的测试代码。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;br /&gt;### 编写用于驱动故事运行的测试代码 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ gedit features/step_definitions/user_steps.rb&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 添加如下代码，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Then /^应该有封激活帐号的邮件发送至&amp;lt;(.+)&amp;gt;$/ do |email|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user = User.find_by_email(email)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user.activation_token.should_not be_blank&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; sent = ActionMailer::Base.deliveries.last&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; sent.to.should eql([user.email])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; sent.subject.should =~ /激活/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; sent.body.should =~ /#{user.activation_token}/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Given /^我已经使用&amp;lt;(.*)\/(.*)\/(.*)&amp;gt;注册过$/ do |username, email, password|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @valid_attributes = {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :username&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; username, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :email&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; email, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; password, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password_confirmation =&amp;gt; password&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.create!(@valid_attributes)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; When /^我访问&amp;lt;(.*)&amp;gt;邮件中激活帐号的链接$/ do |email|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; user = User.find_by_email(email)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; visit activate_url(:token =&amp;gt; user.activation_token)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 故事用例基本上涵盖了我们开发的用意，测试代码准备就绪，还等什么，赶紧跑起来看看吖。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 运行测试，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ ruby script/cucumber -l zh-CN features/user_signup.feature&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SdIJ2LZTYyI/AAAAAAAAAZk/I47r9hMGZuM/user_demo_error_13.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试未能通过，原本应该有封激活帐号的邮件发送至&amp;lt;xuliicom@gmail.com&amp;gt;，然而却没有，因为我们还没有编写用于发送激活邮件的代码。习惯了玩测试的话，测试结果无疑对指导你的编码工作非常有帮助！&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 接下来，我们就来做这些工作。&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;br /&gt;### 添加激活码字段 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 怎么知道用户有没有激活帐号呢？答案是在 users 表中增加用于标识用户帐号是否激活的两个字段，一个用来存放激活码，另一个用来记录帐号激活时间。假设这两个字段分别是 activation_token 和 activated_at，如果 users.activation_token 字段有值，那么就说明用户还没有激活，如果 users.activation_token 为空且 users.activated_at 有值，那么就说明用户已经激活过了。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 下面来添加这组字段，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/generate migration EmailConfirm&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit db/migrate/*_email_confirm.rb&lt;br /&gt;&lt;/font&gt;&lt;/b&gt;&lt;font color="#999999"&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; class EmailConfirm &amp;lt; ActiveRecord::Migration&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def self.up&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; add_column :users, :activation_token, :string&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; add_column :users, :activated_at, :datetime&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def self.down&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; remove_column :users, :activated_at&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; remove_column :users, :activation_token&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ rake db:migrate&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ rake db:test:prepare&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 表结构准备完毕后，再来生成用户注册时的激活码。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 生成激活码——activation_token ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; before_create :initialize_salt, :encrypt_password, :initialize_activation_token&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 生成并返回标识码&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def generate_token&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; encrypt(Time.now.to_s.split(//).sort_by {rand}.join)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 生成激活码&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def initialize_activation_token&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if new_record?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.activation_token = generate_token&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 数据模型搞定后，再从路由下手，需要指定控制器该如何分配响应请求。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 配置激活帐号的路由——activate_url ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit config/routes.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 修改后routes.rb文件内容如下，&lt;br /&gt;&lt;font color="#999999"&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ActionController::Routing::Routes.draw do |map|&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; map.with_options :controller =&amp;gt; 'users' do |page|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; page.signup '/signup', :action =&amp;gt; 'new'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; page.activate '/activate/:token', :action =&amp;gt; 'activate'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; map.resources :users&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 此时，如果你不清楚接下来要做什么；不妨运行测试，测试结果会告诉你答案。由于笔者知道会失败也知晓接下里该做什么，所以就略过此步；因为Model和Route都准备完毕，是时候动手编写业务流程了。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果你用Rails发过邮件，下面的步骤你一定很熟悉。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 生成邮件 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ ruby script/generate mailer UserMailer confirm&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user_mailer.rb&lt;br /&gt;&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; class UserMailer &amp;lt; ActionMailer::Base&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def confirm(user, sent_at = Time.now)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; subject&amp;nbsp;&amp;nbsp;&amp;nbsp; '请激活您的帐号'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; recipients user.email&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; from&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 'Admin'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; sent_on&amp;nbsp;&amp;nbsp;&amp;nbsp; sent_at&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; body&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :username =&amp;gt; user.username, &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :url&amp;nbsp; =&amp;gt; activate_url(:token =&amp;gt; user.activation_token)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/views/user_mailer/confirm.erb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 亲爱的 &amp;lt;%=@username%&amp;gt;：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 您的帐号已经创建成功，请点击下面的链接激活您的帐号：&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%=link_to @url, @url%&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 发送邮件 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 用户注册成功之后，需要发送一封确认邮件到用户注册时填写的电子邮箱中。虽然可以在 User 模型中添加 after_create 的一个回调代码来执行，但这样就给 User 模型类增添了本不应该承担的责任；我们只需要 User 模型提供数据，而不是将发送邮件的任务丢给它。这时候 ActiveRecord 提供的 Observer 就可以派上用场了，使用Observer的好处是它可以将自身连接到模型类中并注册为回调，却无需修改任务模型类的代码，我们将其称之为观察器（是否联想到Ruby设计模式中的观察者模式，呵呵）。下面，我们针对 User 模型创建一个观察器：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/generate observer User&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user_observer.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; class UserObserver &amp;lt; ActiveRecord::Observer&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def after_create(user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; UserMailer.deliver_confirm(user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 然后在 config/environment.rb 注册这个 Observer。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit config/environment.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;config.active_record.observers = :user_observer&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 再次发动测试引擎，看看是否working，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_signup.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh3.ggpht.com/_JmYLjCQND0c/SdINlZAXJXI/AAAAAAAAAaE/77fQd_F_YYA/user_demo_error_14.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 由于在生成邮件那一章节里，激活链接我们用的是 link_url 这种形式，如果你知道 link_url 和 link_path 的区别，那么根据上面的测试结果，你应该了解出错的原因。如果不了解，笔者在这里补充下，link_url 会在链接中加上协议名、主机名和端口号这些；而 link_path 则不用，它会直接用根目录“/”代替之；也就是说， link_url 会在链接中加上网址；又或者说，link_url 采用绝对路径，而 link_path 采用相对路径。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 考虑到现实中的用户注册，系统会发送一封包含网址的邮件到注册用户的邮箱中，我们之前的邮件模板里不得不采用 link_url 这种形式。结合测试结果来看，也许此时您已经意识到，我们是不是忘了配置主机名呢？&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 恭喜您！您确实猜对了。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 配置邮件中激活链接的绝对路径 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user_mailer.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;default_url_options[:host] = HOST&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在 config/environments/test.rb 和 config/environments/development.rb 这两个配置文件中定义 HOST 常量，为了开发和测试需要，这里设置成localhost就可以了。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;HOST = 'localhost:3000'&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 不过在 config/environments/production.rb 中，HOST 常量的值就必须是真实的主机名了。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 另一种方法无需修改app/models/user_mailer.rb和定义HOST常量，直接在各environment/各文件或environment.rb中配置就行了，如下代码&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit config/environment.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;config.action_mailer.default_url_options = { :host =&amp;gt; 'localhost:3000' }&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这样做的好处是只需修改一处。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 好了，补上这个配置，再运行测试，看看有什么不同。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_signup.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SdIOuVLublI/AAAAAAAAAao/t1tF9EIyhPg/user_demo_error_15.png" /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 激活帐号 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 看来我们的邮件能够成功发送了，不过好像访问邮件中的确认链接时出了点问题，根据调试信息“ActionController::UnknownAction”显示，应该是没有找到激活帐号的具体行为（action）。在前面的开发中，我们真的就还没有编写响应用户激活帐号的相关代码，想必此时我们都清楚该做哪些工作了。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们需要给 UserController 类添加一个 Action 来响应用户激活帐号的请求。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;$ gedit app/controllers/users_controller.rb&lt;br /&gt;&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 之前我们在config/routes.rb文件中定义了activate_path，且该activate_path 的 :action 参数指向 activate 方法；于是乎，activate 就是我们需要在 UserController 类中添加的 action。activate方法的代码如下：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def activate&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if @user = User.find_by_activation_token(params[:token])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if !@user.activated?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.email_confirm!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash.now[:notice] = '恭喜您，帐号激活成功！'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 仔细观察 UserController#activate，我们还需要在 User 模型中编写 activated? 和 email_confirm! 这两个实例方法，前者用来确认用户的帐号是否已经激活过，后者则用来激活用户的帐号。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在 protected 之前添加如下两个方法：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 检查是否已经激活&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def activated?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 当 activation_token 为 nil 时表示用户帐号已经激活&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; activation_token.nil?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 激活帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def email_confirm!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; update_attributes(:activation_token =&amp;gt; nil, :activated_at =&amp;gt; Time.now)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 运行测试看看，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_signup.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/Sc5NkY3Re1I/AAAAAAAAATY/q9RbAH4LRdw/user_demo_error_16.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 看来是没有找到模板文件，在此补上用户成功激活帐号的页面。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/views/users/activate.html.erb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存即可。运行测试：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_signup.feature&lt;br /&gt;&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; OK，测试通过！如图，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SdIPfiVzcnI/AAAAAAAAAa4/r_MlKFCZXnw/user_demo_ok_17.png" /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 亲临现场 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 最后开发人员自己别忘了手工测试，以确保万无一失。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 先清除数据库中的记录，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/console&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&amp;gt;&amp;gt; User.delete_all&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假设我们以404为用户名成功注册后，我们来看看数据库中404的activation_token字段是否有值。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&amp;gt;&amp;gt; User.find_by_username('404', :select =&amp;gt; "username, activation_token, activated_at")&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="user_demo_ok_18.png" /&gt;&lt;img style="max-width: 800px;" src="http://lh3.ggpht.com/_JmYLjCQND0c/Sc5OymJuz-I/AAAAAAAAAUA/R0P4i3-2_3w/user_demo_ok_18.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 可以看到，activation_token 的值是一串加密后的字符，activated_at值为空，这说明程序已经给注册用户生成了激活码，而且此时用户还没有激活帐号。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当我们注册成功后，打开邮箱却并没有看到激活帐号的邮件，这是怎么回事呢？&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 因为测试程序跑到是test环境，而我们手工测试的时候，程序是运行在development环境下的，我们没有针对development环境配置邮件服务器。下面我们采用SMTP的发信方式，这里的SMTP SERVER用的是GMAIL，而且是SSL验证登录方式；三次握手，发信速度没sendmail那么快，呵呵！&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit config/environment.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; config.action_mailer.delivery_method = :smtp&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; config.action_mailer.default_charset = 'utf-8'&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; config.action_mailer.smtp_settings = {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :address&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'smtp.gmail.com',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :port&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 25,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :domain&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'YOUR_DOMAIN',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :user_name&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'YOUR_GMAL_USERNAME',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'YOUR_GMAIL_PASSWORD',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :authentication&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'login',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :enable_starttls_auto =&amp;gt; true&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 该配置中大写部分自行替换即可。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 清空users表，我们重新注册404这个用户。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/console&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&amp;gt;&amp;gt; User.delete_all&lt;br /&gt;&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 然后去邮箱看看，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/Sc5POqU71TI/AAAAAAAAAUM/6wsHu1L_7_E/user_demo_ok_19.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这回我们打开邮箱看到了激活帐号的邮件信息，不过邮件内容中的链接标签没有生效，我们期望发送到用户邮箱的是HTML格式的邮件。ActionMailer可以让我们发送多种格式的邮件，只需要按相应的内容类型修改邮件模板的文件名格式即可。基本上，邮件模板的文件名的格式像这样：name[.content.type].renderer；content.type 可选，缺省情况下为文本格式，你也可以手工指定为 text.plain，要发送HTML格式的邮件就需要指定为 text.html；文件后缀 renderer 一般情况下都是 erb（如果你用了HAML插件，模板后缀名应该是haml）。下面我们将之前文本格式的邮件模板修改为网页形式的：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ mv app/views/user_mailer/confirm.erb app/views/user_mailer/confirm.text.html.erb&lt;br /&gt;&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 再次清空users表，重新注册404这个用户，然后前往邮箱看看我们收到的邮件是否是网页格式的。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/Sc5PjrdzFEI/AAAAAAAAAUY/Jsli6H6s4zQ/user_demo_ok_20.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们看到激活帐号的超链接生效了(没有将超链接标签明文显示)，这说明系统发送出去的确实是HTML邮件。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 接下来我们点击邮件中的链接来到了激活帐号的页面，我们看到帐号激活成功的提示信息。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/Sc5QNO1JhpI/AAAAAAAAAU0/8zgYeY9OJ2o/user_demo_ok_21.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果激活成功，数据库中的activation_token字段应该是空值，且activated_at字段的值应该为一时间戳；在之前的程序中，我们确实是按此逻辑编码的。虽然测试成功，而且我们也非常顺利地亲历了一遍注册流程，那么是否就说明我们的应用程序没有程序上的漏洞了吗？我们真的激活帐号了吗？我们不妨看看数据库这只黑匣子，此时应该是让数据说话的时候了。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ ruby script/console&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&amp;gt;&amp;gt; User.find_by_username('404', :select =&amp;gt; "username, activation_token, activated_at")&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/Sc5QarNLchI/AAAAAAAAAVA/O06bkof8LhA/user_demo_error_22.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 哎呀！记录居然没被更新，看来我们被表面现象给忽悠了。我想你此时也和我一样迷惑，为什么数据记录没有被更新呢？这中间到底发生了什么？这让我不由自主地想象起来，也许Rails的ORM真的修改了User实例对象的activation_token和activated_at属性的值，只不过还没有成功地写入到数据库里边而已。果真如此吗？如何证明这一说法成立呢？我们来看看User模型类的 email_confirm! 方法，下面是email_confirm! 方法的源码：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 激活帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def email_confirm!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; update_attributes(:activation_token =&amp;gt; nil, :activated_at =&amp;gt; Time.now)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们知道，update_attributes 方法还有一个和自己长得差不多一样的方法，即 update_attributes!&lt;br /&gt;；后者比前者仅仅多一个感叹号而已，两者都是更新当前模型对象所指向的数据记录，只不过前者更新失败会返回false，后者更新失败则会抛出异常信息并停止程序运行，我们不妨用 update_attributes! 替换 email_confirm!方法中的update_attributes，如果问题真的出现在这里，至少我们也可以看见抛出的错误信息，这些错误调试信息对开发人员来说是那么的重要。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user.rb&lt;br /&gt;&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 修改 email_confirm! 方法如下：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def email_confirm!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; update_attributes!(:activation_token =&amp;gt; nil, :activated_at =&amp;gt; Time.now)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存，然后重新访问或刷新激活帐号的页面，我们看到系统捕获到了非常实用的情报，如图：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/Sc5Q9-2j6RI/AAAAAAAAAVM/x2gg6PbC0e0/user_demo_error_23.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 看来问题还真的出在User模型类的email_confirm!方法这里，当程序尝试更新 activation_token 和 activated_at 这两个字段时，系统告诉我们密码不能为空并就此打住，程序抛出错误并停止执行，后面当然不会更新数据库里边的记录了。找到出错的原因后，我们马上就明白 update_attributes（或update_attributes!） 会更新当前对象所指向的记录的所有字段，并在更新之前执行数据校验，如果校验失败就会打断程序的运行。想到此，针对问题的解决方案也初现轮廓，只要程序更新指定的字段，并在更新这些指定字段的时候不去校验其他字段的数据有效性就行了。OK，我们有非常适合用于email_confirm!的替代写法，不妨修改email_confirm!方法如下：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 激活帐号&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def email_confirm!&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.activation_token = nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.activated_at = Time.now&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; save(false)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 上述代码中的 save 方法会更新这些字段的值，第一个参数的值指明为false后将不会执行数据校验，看来这一切和我们的想法非常吻合，不妨保存user.rb再刷新几次浏览器看看。第一次刷新和我们初次访问激活链接看到的效果一样，都是提示帐号激活成功，后面几次就看不到激活成功的消息了，因为帐号只需要激活成功一次就足够了，效果确实很理想，我们去访问下数据库让它给我们做个见证。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/console&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&amp;gt;&amp;gt; User.find_by_username('404', :select =&amp;gt; "username, activation_token, activated_at")&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/Sc5RRjd_aGI/AAAAAAAAAVY/_DGIR1xSdEM/user_demo_ok_24.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 哈哈，数据记录已经更新了，这意味着程序已经可以按照我们之前的意愿运行了。用户提交注册资料后会收到一封关于激活帐号的邮件，然后点击其中的链接可以成功激活他的帐号。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 至此，我们在 email_activation 分支上的开发工作已经顺利完成，可以将工作成果归并到主干中去了。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 提交工作成果到GIT仓库 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git add .&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git commit -m &lt;/font&gt;&lt;/b&gt;&lt;font color="#999999"&gt;"&lt;/font&gt;&lt;font color="#999999"&gt;People can activation their accounts by the confirm emails.&lt;/font&gt;&lt;font color="#999999"&gt;"&lt;/font&gt;&lt;b&gt;&lt;font color="#999999"&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git checkout master&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git merge email_activation&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git branch -d email_activation&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ git tag v2&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; （注意，真正的开发中可不是到功能开发完毕了才commit，而是边开发边add和commit。为了方便演示编码过程，文章中没有一一列举。）&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 小结 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在这篇教程中，我们的开发工作遇到了不小的挫折，尤其是在人工测试那里，经过我们自己动手测试后，才知晓我们的程序漏洞百出。之所以这样，是由于笔者有意而为之，其实笔者的用意非常简单，就是想告诉开发者亲临现场做人工测试的重要性。也许确实让您受挫了，觉得好像是为了测试而测试似的；大可不必有如此想法，如果您是位Rails熟手，想必也不会犯那些低级错误，比如update_attributes和save(false)这种区别及其应用场合，也会知晓 test/development/production 这几种环境的区别；那也就避免了些不必要的麻烦。经验是慢慢积累的，过程可以帮我们汲取经验。等您自己应用熟练了，我相信您能体会到测试带来的好处。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 下节预告 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 接下来我们依然是借助cucumber+rspec来驱动用户登录功能的开发，看测试跟session和cookie打交道。如果有兴趣，期待您能够下次光临！如果有好的建议和经验非常希望能够与您交流，您可以在下面发表留言或者和我email联系，我的邮箱是 xuliicom@gmail.com。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/3443179681250049771-3384514496238395337?l=l404.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://l404.blogspot.com/feeds/3384514496238395337/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=3443179681250049771&amp;postID=3384514496238395337' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/3384514496238395337'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/3384514496238395337'/><link rel='alternate' type='text/html' href='http://l404.blogspot.com/2009/03/cucumberrspec-2.html' title='使用Cucumber+Rspec玩转BDD(2)——邮件激活'/><author><name>404</name><uri>http://www.blogger.com/profile/08660239939199305528</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/_JmYLjCQND0c/SXaZ4wfUELI/AAAAAAAAANg/iLlbANMP1F0/S220/8b1111f6-216a-3853-a396-ee4cff0a219c.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://lh6.ggpht.com/_JmYLjCQND0c/SdIJ2LZTYyI/AAAAAAAAAZk/I47r9hMGZuM/s72-c/user_demo_error_13.png' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-3443179681250049771.post-8562349538958730582</id><published>2009-02-22T22:22:00.025+08:00</published><updated>2009-04-02T15:58:33.380+08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Cucumber'/><category scheme='http://www.blogger.com/atom/ns#' term='Rspec'/><category scheme='http://www.blogger.com/atom/ns#' term='TDD'/><category scheme='http://www.blogger.com/atom/ns#' term='Rails'/><title type='text'>使用Cucumber+Rspec玩转BDD(1)——用户注册</title><content type='html'>&lt;b&gt;&lt;br /&gt;### 引言 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试驱动开发的美名即TDD（Test-Driven Development 的缩写）；顾名思义，就是利用测试来驱动程序的设计及其实现。在这一过程中，先写测试程序，然后再编码使其通过测试，经过几次反复的迭代后，使得程序的实用性达到开发人员的理想需求。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Rspec是一种实现BDD的工具，倡导在编写测试的同时描述代码的行为，BDD即行为驱动开发（Behaviour-Driven Development），从TDD进化而来，算是TDD的一个分支，使用Rspec编写的测试代码更加灵活也更具有趣味性。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Cucumber继承了BDD的先进理念并由此衍生出了SDD（Story-Driven Development）这一特性，可以说是继承并发扬了BDD的DSL特点，从而使得测试中的行为更加具有连贯性和观赏性，当一系列的行为串起来，一个精彩生动的故事就应运而生了。Cucumber以故事用例为单位，每个故事对应一小段测试代码（可重用），每一小块功能需求就写成一段故事描述文本。当你了解这样的规则并应用规则熟练后，把用户的直接需求搬到测试上去就轻而易举了。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果说TDD是发芽期，那么BDD就是鲜花盛开时，到SDD就是该结果了，不是吗？UnitTest的出现就像测试兴起之初露出的小芽尖儿；Rspec / Shoulda的流行意味着测试春天的悄然而至；后来盼来了个硕果——Cucumber，英译汉即黄瓜；还等什么，赶紧搞根黄瓜尝尝！&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 创建一个新的项目 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ cd ~/code&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ rails user_demo &amp;amp;&amp;amp; cd user_demo&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 可以在这里下载源码：&lt;a target="_blank" href="http://github.com/404/bdd_user_demo/tree/master"&gt;http://github.com/404/bdd_user_demo/tree/master&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 功能需求 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在进行后面的开发之前，先来预告本节开发工作的主要内容；以下是简要的用户注册功能：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1. 提供一张表单，方便用户填写注册资料；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2. 用户填写填写注册资料并提交后，若信息填写有误，则提示用户注册失败，并返回注册页面；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 3. 用户填写填写注册资料并提交后，若信息填写正确，提示用户注册成功，并跳转到用户资料页面。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 非常简单，是不是？下面我们就按cucumber要求的形式将以上的功能需求转化成一个个故事用例。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 编写用户注册的故事用例 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ ruby script/generate cucumber&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 事先需要安装 Cucumber 及其依赖包，如尚未安装，可以运行 gem install cucumber 来安装。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 接下来在 features/ 目录下面创建一个 user_signup.feature 文件，用来写我们期望的故事（功能需求）；你可以选择任意一款你喜欢的编辑器来填充这个文件（为方便演示，文章中新建或修改文件均用gedit）。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ gedit features/user_signup.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 功能: 注册成为网站会员&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 为了能够浏览网站只对在线会员可见的那些内容&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 作为一名访客&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我希望注册成为网站会员&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户填写无效数据并注册&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我来到用户注册页面&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;用户名&amp;gt;中输入&amp;lt;invalid username&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;电子邮箱&amp;gt;中输入&amp;lt;invalid email&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;密码&amp;gt;中输入&amp;lt;password&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;确认密码&amp;gt;中输入&amp;lt;verify password&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我按下&amp;lt;注册&amp;gt;按钮&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;注册失败&amp;gt;的提示信息&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 场景: 用户填写正确的数据并注册&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当 我来到用户注册页面&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;用户名&amp;gt;中输入&amp;lt;404&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;电子邮箱&amp;gt;中输入&amp;lt;xuliicom@gmail.com&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;密码&amp;gt;中输入&amp;lt;password&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我在输入框&amp;lt;确认密码&amp;gt;中输入&amp;lt;password&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且 我按下&amp;lt;注册&amp;gt;按钮&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 那么 我应该看到&amp;lt;注册成功&amp;gt;的提示信息&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 上面就是我们写好的故事剧本，关于用户注册的；不仅开发人员易懂，而且也接近PM给我们的书面需求了（有时候你可能会写得更详细）。当我们按照这种约定俗成的格式线性地描述一番后，Cucumber的故事驱动器会虚拟出一个故事的主人公（比如叫Robot），Robot会亲历每个场景，并遵循当前故事场景中的每个情节扮演每一个动作，每个动作的成败都会被Cucumber记录下来，等到演出结束后汇报给我们详细的数据。那么你可能会好奇Robot该如果演出我们写好的情节呢？我们写好了故事剧本，Robot到底有没有按照我们的意思去演，我们是否可以要求他按照我们的想法去做？其实里面的Robot比较老实本份，你不给他说清楚细节以及给出相关的道具，他会呆在那里傻呼呼的望着你，一旦万事俱备，你只要点点头，他将以光速前进（010110101...）。为了让Robot完成我们的意愿，还需要做些事情，就是告诉他如何进场以及每个细节动作的规范。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 编写用于故事运行的测试代码 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ gedit features/step_definitions/user_steps.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; When /^我来到(.+)$/ do |page_name|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; visit path_to(page_name)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; When /^我在输入框&amp;lt;(.+)&amp;gt;中输入&amp;lt;(.*)&amp;gt;$/ do |field, value|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; fill_in(field, :with =&amp;gt; value)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; When /^我按下&amp;lt;(.+)&amp;gt;按钮$/ do |button|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; click_button(button)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Then /我应该看到&amp;lt;(.+)&amp;gt;的提示信息/ do |msg|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; response.body.should =~ Regexp.new(msg)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在第1小段代码中，我们将测试中的当前行为引导至正则匹配到的页面那里；这里正则匹配到的页面名称应该是“用户注册页面”，但测试程序并不知道该页面的访问路径，所以我们需要手工声明一下。还好，Cucumber生成的文件中为我们准备好了path_to()方法，我们只要根据需要修改原生的features/support/paths.rb这个文件就可以了，修改后的代码如下：&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ gedit features/support/paths.rb&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; def path_to(page_name)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; case page_name&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; when /首页/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; root_path&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; when /用户注册页面/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; signup_path&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # Add more page name =&amp;gt; path mappings here&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; else&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; raise "Can't find mapping from \"#{page_name}\" to a path."&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 参照以上的流程控制，你可以根据需要添加更多的页面路径；当我们的故事越来越多的时候，比如用户登录页面呀找回密码页面呀什么的，我们会频繁地在path_to()方法中增加更多的分支。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 上面几个简单的步骤已经把用户注册的这个需求功能写明白了，同时还设计好了描述故事主人公行为的代码，我们不妨运行下测试，测试结果会告诉我们接下来该做什么。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 观测试了解工作内容 ###&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ ruby script/cucumber -l zh-CN features/user_signup.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SdHaLfJHvuI/AAAAAAAAAX0/qd5VupyG3P0/s576/user_demo_error_01.png" /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 可以看到测试没有通过，两个演出场景都以失败告终。红色的表示所在的故事情节没有通过测试，并抛出了错误信息（准确地说应该是非常实用的调试信息）。只有当尖括号括起来的文本颜色为绿色时，才是测试通过了的故事情节。根据抛出的错误信息可以知晓，不仅用户注册也没未找到，而且 signup_path 这个路径也未定义，其页面表单项自然也就不存在了。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 到目前我们只写好了用户注册的故事以及模拟该故事演示的脚本，对于我们的应用程序，我们还没有真正开始编码，也只能看到这一堆花花绿绿的出错信息了。如果你习惯了的话（你会喜欢的），我想你不会把这些信息当作出错信息来看，至少它们能告诉我们少作了什么以及该做哪些事情，这是非常有价值的情报，它能把最终用户的需求用代码的形式直观地呈现在开发人员的眼前。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 看来我们真正的目标该现身了！我们需要根据测试未通过的调试信息来指引我们开发正确并真实的用户注册程序。测试程序没有找到注册页面，此时我们可以用Rails内置的生成器一同创建。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 使用生成器快速实现程序结构 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ ruby script/generate rspec&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ ruby script/generate rspec_model User username:string email:string encrypted_password:string password_salt:string&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ rake db:migrate&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ rake db:test:prepare&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ ruby script/generate rspec_controller Users new create show&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 短短几行命令，就创建了User模型和users控制器以及控制器中的3个方法；请求new()方法会返回用户注册页面（也是之前测试程序要访问的），create()方法用来收集并递交用户填写的注册数据，show()方法是新用户注册成功后用来显示用户资料的页面。除此之外，Rspec生成器还帮我们创建了几个配套的测试文件，后面我们会用到。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 光有程序结构还不行，还得告诉终端用户在注册页面的访问路径。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 布置路由（Routes） ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ gedit config/routes.rb&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ActionController::Routing::Routes.draw do |map|&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; map.signup '/signup', :controller =&amp;gt; 'users', :action =&amp;gt; 'new'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; map.resources :users&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如上述修改后的routes.rb，我们配置了注册页面的具名路由——signup，还有给users这组资源加上了标准的REST。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 牵好线，搭好桥后；再来告诉程序应如何处理用户注册。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 处理用户注册的业务流程 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ gedit app/controllers/users_controller.rb&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; class UsersController &amp;lt; ApplicationController&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def new&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.new&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def create&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.new(params[:user])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if @user.save&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash[:notice] = '注册成功！'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; redirect_to(@user)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; else&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; flash[:notice] = '注册失败！'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; render :action =&amp;gt; "new"&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def show&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.find(params[:id])&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 布局模板 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在修改用户注册页面之前，来段小插曲，我们新建一个公共模板页面，里面放些用于程序呈现提示消息的一些内容。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/views/layouts/application.html.erb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;head&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;meta http-equiv="content-type" content="text/html;charset=UTF-8" /&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;title&amp;gt;&amp;lt;%= controller.controller_name %&amp;gt;#&amp;lt;%= controller.action_name %&amp;gt;&amp;lt;/title&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/head&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;body&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% if flash[:notice] -%&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p style="color: green"&amp;gt;&amp;lt;%= flash[:notice] %&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% end -%&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= yield %&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/body&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/html&amp;gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们在公共模板页添加了用于显示提示信息的代码，这样无论用户注册成功与否，我们都能看到相关的提示信息。接下来修改用户注册页面填充一张供用户填写注册资料的表单项，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;$ gedit app/views/users/new.html.erb&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;h1&amp;gt;注册新用户&amp;lt;/h1&amp;gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% form_for(@user) do |f| %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= f.error_messages %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= f.label :username, '用户名' %&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= f.text_field :username %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= f.label :email, '电子邮箱' %&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= f.text_field :email %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= f.label :password, '密码' %&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= f.password_field :password %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= f.label :password_confirmation, '确认密码' %&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= f.password_field :password_confirmation %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%= f.submit "注册" %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;% end %&amp;gt;&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 页面准备完毕，我们将其用浏览器打开看看是否是我们想要的样子。启动 Web Server，&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ ruby script/server&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在浏览器的地址栏中输入 http://localhost:3000/signup 打开注册页面。输出，&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SdHcxnNJDmI/AAAAAAAAAYM/MoNppHzNCdc/user_demo_error_02.png" /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们看到出错了；提示在User模型的实例中没有找到password这个方法。回顾之前的操作，我们用rspec_model生成器创建User数据模型的时候确实没有指定password这个字段；因为数据库不保存明文密码，所以我们指定了encrypted_password这个数据字段，当把明文密码加密后才存到数据表的此列中。我们需要修改User模型，给它加上明文密码属性password和确认密码password_confirmation。&lt;br /&gt;&lt;font color="#999999"&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;$ gedit app/models/user.rb&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; class User &amp;lt; ActiveRecord::Base&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; attr_accessor :password, :password_confirmation&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 刷新注册页面，这次能够正常显示了。如图：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SdHcQ8PkQFI/AAAAAAAAAYA/bNTFS36MxBY/user_demo_ok_03.png" /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当新用户注册成功后，会跳转到显示用户信息的页面，根据需要呈现的信息，我们对这个页面作一些修改。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ gedit app/views/users/show.html.erb&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;h1&amp;gt;用户资料&amp;lt;/h1&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;b&amp;gt;用户名:&amp;lt;/b&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%=h @user.username %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;p&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;b&amp;gt;电子邮箱:&amp;lt;/b&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;%=h @user.email %&amp;gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;lt;/p&amp;gt;&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 再次运行测试看看～&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/cucumber -l zh-CN features/user_signup.feature&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 可以看到，有一个步骤没有通过测试，如下图：&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SdHdz5qk7CI/AAAAAAAAAYY/7ZVDo3XiUME/s512/user_demo_error_04.png" /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 根据以上图中的调试信息，我们看到在第一个场景中，测试程序使用无效的数据注册后并没有返回“注册失败”的提示信息，这意味着不管用户有无填写注册数据，抑或注册数据无效都可以成功注册。天啊，那是多么严重的Bug，此时此刻，我们应该赶紧补上这个漏洞。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 修改 Model，增加一些必要的数据校验 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ gedit app/models/user.rb&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; class User &amp;lt; ActiveRecord::Base&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 因为数据库保存的密码是加密后的，&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 为了设计完整的User模型，&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 在这里需手工声明 password 和 password_confirmation 属性；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 当然，app/views/users 中的部分模板也会用到这两个属性；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 另一个好处是为了方便测试。&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; attr_accessor :password, :password_confirmation&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 密码长度不小于4或大于40个字节&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; validates_length_of :password, :within =&amp;gt; 4..40&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 用户两次密码输入必须一致&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; validates_confirmation_of :password&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 必填项&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; validates_presence_of :username, :email, :password&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 用户名和密码唯一&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; validates_uniqueness_of :username, :email&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 必须是有效的email格式&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; validates_format_of :email,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :with =&amp;gt; /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当在 User 模型中增加这些数据校验代码后，我们的漏洞应该算是补上了，不妨测试看看：&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;font color="#999999"&gt;&lt;b&gt;$ ruby script/cucumber -l zh-CN features/user_signup.feature&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SdHecBn_huI/AAAAAAAAAYk/IrZW9IlQi1Y/user_demo_ok_05.png" /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 可以看到，2个场景中的14个情节步骤全部通过，哈哈!是不是有点成就感呢？几个回合之后，在用户注册这个故事用例的测试驱动下，我们一步一步编写了真实有效的用户注册程序；对！就是客户需要的用户注册功能，简直就是贴近客户口头需求的应用开发呀！&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 到此，第一根黄瓜啃完了，如果你此时非常满足可以邀请你的客户过来试用了，然后抿口绿茶后帮助客户打开注册页面，同时在一旁观察享受着你的工作成果。客户按照之前口头跟你说的那个用户注册故事，第一次故意填入无效的数据并提交注册，哎呀，返回一些错误信息，接着使用不同的错误数据多试了几次。好几次后，客户看了看错误信息并按要求填入正确的数据并提交注册，跳转到显示用户资料的页面并提示注册成功，客户对你的工作成果非常满意，赞叹你的工作效率是如此的高效！&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 亲临现场 ###&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 不过，作为一个开发人员，404强烈建议在交付给客户查看之前开发人员最好亲临现场手工测试一边，以防万一，正所谓慎在于畏小；若有闪失，客户大概会提醒你们改进，但若因为一丁点儿小错误导致难以处理的麻烦就得不尝失了，比如黑客剽窃了所有没有加密的信用卡密码，这下可就丢大了。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 为了不必要的麻烦，我们自己来手工测试一遍！毕竟是开发人员，测试起来更了解细节想到的也会周全些。我们打开注册页面，试了几次填入无效的数据并注册，结果如我们想象中一样失败了；然后我们填入正确的数据并注册，我们看到注册成功的友好信息。但我们看到的仅仅是注册成功的表象，要透过现象看本质，就得了解注册成功的确切证据是什么？最有说服力的是数据，我们不妨查看写入到数据库中的数据是否是我们想要的结果。&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 假设我们之前以用户“mouse”成功注册，我们来查询下数据库看看他填写的数据是否符合我们设定的标准。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 检查入库数据是否有效，&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;$ ruby script/console&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;gt;&amp;gt; User.find_by_username('mouse')&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh3.ggpht.com/_JmYLjCQND0c/SaTqFV4Fr8I/AAAAAAAAAP0/BFs5nzX4cj0/user_demo_error_06.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 根据输出结果，我们看到 “encrypted_password: nil, password_salt: nil”这行，这确凿无疑地说明密码根本没有保存到数据库中。此时你是否想起了还没有加密明文密码的这一过程？这样的情况若是出现在之前假设的信用卡例子中，后果将不堪设想。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 不测试的代码不是合格的代码，不写测试的程序员不是好的程序员哦；只有开发人员亲历过的测试才算得上可靠的测试。切记！&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 亡羊补牢，为时未晚；好在我们还处于开发进行时，这点漏洞我们可以立马补上。经过前面的教训，我们知道Cucumber只不过是按流程走过场而已，这个流程也就是模拟MVC总终端到处理数据的模式，Cucumber 的出现一定程度上集成了Rspec-Rails测试框架中对View和Controller以及Model的一条龙测试，不过在校验数据的粒度测试上，Cucumber却显得相形见绌，还没单元测试好用；试想每当验证某个数据是否合格时，是写个场景好呢还算写一两行代码方便？，显然是后者咯！这种对比不过是寸有所长，尺有所短罢了，只要我们集每种工具所长用得恰到好处就行，就如同协调一个优秀的团队。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 考虑到Cucumber和BDD结合慎密，这里将选用Rspec-Rails作为后发测试工具，Rspec/Rspec-Rails这样优秀的测试框架在对数据模型的测试已经非常成熟，这这篇文章中，或许还用不到Rspec的一些高级特性，不过用最简单的技术做出有价值的事情就已经非常不错了！&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 通过Rspec-Rails测试UserModel完善数据校验 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Rspec-Rails是一款我们熟悉的针对Rails的BDD测试框架，为了补上前面提到的那个未加密的漏洞，我们习惯性的采用了测试先行的策略。&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit spec/models/user_spec.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 修改过的测试代码如下：&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; describe User do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; before(:each) do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @valid_attributes = {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :username&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; '404',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :email&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'xuliicom@gmail.com',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'password',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password_confirmation&amp;nbsp;&amp;nbsp; =&amp;gt; 'password'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.new(@valid_attributes)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it "should encrypt the password before saving to the database" do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.save&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.password_salt.should_not be_nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.encrypted_password.should_not be_nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;$ ruby script/spec spec/models/user_spec.rb&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SaTqFX67IQI/AAAAAAAAAP8/slc1TTxcZw8/user_demo_error_07.png" /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们看到测试失败了，这不得不督促我们应该做点事情使得测试能够顺利通过，我们可以从编写密码加密的相关代码着手。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ gedit app/models/user.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 修改后的user.rb文件内容如下：&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; require 'digest/sha1'&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; class User &amp;lt; ActiveRecord::Base&lt;br /&gt;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 因为数据库保存的密码是加密后的，&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 为了设计完整的User模型，&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 在这里需手工声明 password 和 password_confirmation 属性；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 当然，app/views/users 中的部分模板也会用到这两个属性；&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 另一个好处是为了方便测试。&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; attr_accessor :password, :password_confirmation&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 密码长度不小于4或大于40个字节&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; validates_length_of :password, :within =&amp;gt; 4..40&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 用户两次密码输入必须一致&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; validates_confirmation_of :password&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 必填项&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; validates_presence_of :username, :email, :password&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 用户名和密码唯一&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; validates_uniqueness_of :username, :email&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 必须是有效的email格式&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; validates_format_of :email,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :with =&amp;gt; /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 钩子方法，保存之前生成 password_salt&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 并使用 password_salt 和原始密码来加密生成新密码&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; before_create :initialize_salt, :encrypt_password&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 生成加密后的字符串并返回&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def encrypt(string)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; generate_hash("--#{password_salt}--#{string}--")&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; protected&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 加密指定的字符串并返回&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def generate_hash(string)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Digest::SHA1.hexdigest(string)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 生成 password_salt并返回&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def initialize_salt&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if new_record?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.password_salt = generate_hash("--#{Time.now.to_s}--#{email}--")&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#c0c0c0"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 生成加密后的新密码并返回&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; def encrypt_password&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return if password.blank?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; self.encrypted_password = encrypt(password)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 运行测试，看看加密明文密码是否起作用了。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/spec spec/models/user_spec.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh5.ggpht.com/_JmYLjCQND0c/SaTqFVUzylI/AAAAAAAAAQE/tsloH6prglI/user_demo_ok_08.png" /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 测试通过，显然在用户资料被保存之前密码被加密已是事实了！前面讲到Cucumber在对数据校验的粒度测试上不是特别灵活，于是在这里我们可以把对User模型对象的数据校验测试也一同放入user_spec.rb中。&lt;br /&gt;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;$ gedit spec/models/user_spec.rb&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; describe User do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; before(:each) do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @valid_attributes = {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :username&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; '404',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :email&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'xuliicom@gmail.com',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; =&amp;gt; 'password',&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; :password_confirmation&amp;nbsp; =&amp;gt; 'password'&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user = User.new(@valid_attributes)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it 'should be valid' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.should be_valid&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it 'should require a username' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.username = ''&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.should_not be_valid&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.should have(1).errors_on(:username)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it 'should not be valid without a email' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.email = ''&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.should_not be_valid&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it 'should be valid if email past the correct match' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.email.should match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it "should have a unique username and password" do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @first_user = User.create!(@valid_attributes)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user = User.new(@valid_attributes)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user.should_not be_valid&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user.should have(1).errors_on(:username)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @second_user.should have(1).errors_on(:email)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it 'should not be valid without a password' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.password = ''&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.should_not be_valid&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it 'should be valid if password has a minimum of 4 characters' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.password.should have_at_least(4).characters&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it 'should be valid if password has not more than 40 characters' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.password.should have_at_most(40).characters&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it 'should be valid if password and password confirmation match' do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.password_confirmation.should == @user.password&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; it "should encrypt the password before saving to the database" do&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.save&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.password_salt.should_not be_nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; @user.encrypted_password.should_not be_nil&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; end&lt;/font&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;&lt;font color="#999999"&gt;$ ruby script/spec spec/models/user_spec.rb&lt;/font&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SaTqFew0-UI/AAAAAAAAAQM/Tn3YjZBP1oc/user_demo_ok_09.png" /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果你本机配置好了autotest/notifier，可以将Cucumber和Rspec-Rails测试代码一并运行。这里为了方便演示，暂时删除下面几个文件：&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ rm -rf spec/controllers/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ rm -rf spec/helpers/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; $ rm -rf spec/views/users&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;$ AUTOFEATURE=true autospec&lt;/b&gt;&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh4.ggpht.com/_JmYLjCQND0c/SaTqFrhROxI/AAAAAAAAAQU/C5BiHwnNRLA/user_demo_ok_10.png" /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 最后，为了确认没有疏忽还是手工测试一遍好，自己写的东西当然要在第一时间enjoy! 我们不妨再注册一个用户看看。&lt;small&gt;&lt;br /&gt;&lt;br /&gt;&lt;/small&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh6.ggpht.com/_JmYLjCQND0c/SaTqLnZXaXI/AAAAAAAAAQc/ixzgoDVL2wg/user_demo_error_11.png" /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 不好意思呃，没有注册成功（因为“mouse”用户已经存在了，我们前面在User模型里边设置了用户名和email必须唯一），我们换用“mouse2”试试看，正如你所料，注册过程相当顺利。我们看看“mouse2”这位用户的注册资料是否符合我们的要求。&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;br /&gt;&lt;font color="#999999"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;b&gt;$ ruby script/console&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;gt;&amp;gt; User.find_by_username('mouse2')&lt;/font&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;img style="max-width: 800px;" src="http://lh3.ggpht.com/_JmYLjCQND0c/SaTqLvskaPI/AAAAAAAAAQk/MSxh09rpAC4/user_demo_ok_12.png" /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 我们看到密码被加密过了，还生成了一份密码摘要，其他数据也符合我们定义的要求，总算可以松口气，是时候该主动报告我们的工作成果了！&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;### 下节预告 ###&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在本系列的下一篇文章里，404会继续整理以前的一些学习笔记供学友们参考；还是测试的一些内容，您会了解如何用Cucumber测试发送邮件以及邮件激活帐号；额外地，为了更好的管理开发工作，您得用上GIT。希望大家在阅读本系列的同时能及时给出一些反馈或者建议；如果您有更好的想法或者经验心得，非常期望与您交流，您可以在下面留言或者email到xuliicom@gmail.com。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/3443179681250049771-8562349538958730582?l=l404.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://l404.blogspot.com/feeds/8562349538958730582/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=3443179681250049771&amp;postID=8562349538958730582' title='1 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/8562349538958730582'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/8562349538958730582'/><link rel='alternate' type='text/html' href='http://l404.blogspot.com/2009/02/cucumberrspec-1.html' title='使用Cucumber+Rspec玩转BDD(1)——用户注册'/><author><name>404</name><uri>http://www.blogger.com/profile/08660239939199305528</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/_JmYLjCQND0c/SXaZ4wfUELI/AAAAAAAAANg/iLlbANMP1F0/S220/8b1111f6-216a-3853-a396-ee4cff0a219c.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://lh6.ggpht.com/_JmYLjCQND0c/SdHaLfJHvuI/AAAAAAAAAX0/qd5VupyG3P0/s72-c/user_demo_error_01.png' height='72' width='72'/><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-3443179681250049771.post-4438185155247585298</id><published>2009-02-12T09:36:00.015+08:00</published><updated>2009-02-12T20:02:22.913+08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Ubuntu'/><category scheme='http://www.blogger.com/atom/ns#' term='AutoTest'/><category scheme='http://www.blogger.com/atom/ns#' term='TDD'/><category scheme='http://www.blogger.com/atom/ns#' term='Rails'/><title type='text'>Ubuntu8.10上小试Rails-AutoTest</title><content type='html'>1. 下载并安装Mumbles&lt;br /&gt;&lt;br /&gt;$ cd ~/downloads&lt;br /&gt;$ wget http://nchc.dl.sourceforge.net/sourceforge/mumbles/mumbles_0.4-1_all.deb&lt;br /&gt;(该版本不支持自定义图标，好在我们还可以掌控消息标题和消息内容。)&lt;br /&gt;&lt;br /&gt;进入~/downloads目录双击文件mumbles_0.4-1_all.deb进行安装。&lt;br /&gt;&lt;br /&gt;安装成功后启动 Mumbles，点击 应用程序 -&gt; 附件 -&gt; Mumbles（启动后Mumbles的图标会出现在任务栏的右边）；输入：&lt;br /&gt;&lt;br /&gt;$ mumbles-send "test title" "test message"&lt;br /&gt;&lt;br /&gt;这会在右上角紧贴任务栏处显示一块黑底白字，停留几秒后就自动消失了；说明Mumbles已安装成功且能正常使用。&lt;br /&gt;&lt;br /&gt;也可以把Mumbles加入到系统启动项，这样一开机Mumbles就自动运行了；如果不想加入启动项，记得测试之前必须先启动Mumbles。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;2. 下载并解压ruby-dbus，可以从这里下载：https://trac.luon.net/ruby-dbus/&lt;br /&gt;&lt;br /&gt;$ cd ~/downloads&lt;br /&gt;$ wget --no-check-certificate https://trac.luon.net/data/ruby-dbus/releases/ruby-dbus-0.2.1.tar.gz&lt;br /&gt;$ tar -xzvf ruby-dbus-0.2.1.tar.gz&lt;br /&gt;$ cd ruby-dbus-0.2.1&lt;br /&gt;&lt;br /&gt;查阅README文件后按指示进行安装：&lt;br /&gt;&lt;br /&gt;$ ruby setup.rb config&lt;br /&gt;$ ruby setup.rb setup&lt;br /&gt;$ ruby setup.rb install&lt;br /&gt;&lt;br /&gt;启动irb检查ruby-dbus是否安装成功：&lt;br /&gt;&lt;br /&gt;$ irb&lt;br /&gt;irb(main):001:0&gt; require 'dbus'&lt;br /&gt;=&gt; true&lt;br /&gt;&lt;br /&gt;返回true，即ruby-dbus安装成功且能正常使用，可以删除无用文件了。&lt;br /&gt;&lt;br /&gt;$ rm -rf ~/downloads/ruby-dbus-0.2.1&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;3.创建 .autotest 文件&lt;br /&gt;&lt;br /&gt;由于autotest这个自动测试命令基于ZenTest，所以系统需安装ZenTest这个Ruby Gem包才行；若使用 gem list 命令看不到ZenTest项可按如下方式进行安装。&lt;br /&gt;&lt;br /&gt;$ gem install ZenTest&lt;br /&gt;&lt;br /&gt;接下来可以在用户所在的 $HOME 目录或者项目所在的根目录创建一个 .autotest文件；&lt;br /&gt;&lt;br /&gt;$ gedit ~/.autotest&lt;br /&gt;&lt;br /&gt;填充如下代码(&lt;a href="http://gist.github.com/62446"&gt;http://gist.github.com/62446&lt;/a&gt;)：&lt;br /&gt;&lt;br /&gt;require 'dbus'&lt;br /&gt;&lt;br /&gt;def send_message(title, message)&lt;br /&gt;begin&lt;br /&gt;  bus = DBus::SessionBus.instance&lt;br /&gt;  mumbles_service = bus.service("org.mumblesproject.Mumbles")&lt;br /&gt;  mumbles = mumbles_service.object("/org/mumblesproject/Mumbles")&lt;br /&gt;  mumbles.introspect&lt;br /&gt;  mumbles_iface = mumbles["org.mumblesproject.Mumbles"]&lt;br /&gt;  sig = mumbles_iface.signals["Notify"]&lt;br /&gt;  bus.emit(mumbles_service, mumbles, mumbles_iface, sig, title, message)&lt;br /&gt;rescue Exception =&gt; e&lt;br /&gt;end&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;Autotest.add_hook :ran_command do |at|&lt;br /&gt;begin&lt;br /&gt;  results = at.results.last&lt;br /&gt;  unless results.nil?&lt;br /&gt;    output = results[/(\d+)\s+examples?,\s*(\d+)\s+failures?(,\s*(\d+)\s+pending)?/]&lt;br /&gt;    if output&lt;br /&gt;      failures = $~[2].to_i&lt;br /&gt;    end&lt;br /&gt;    if failures &gt; 0&lt;br /&gt;      send_message("Tests Failed", "#{output}")&lt;br /&gt;    else&lt;br /&gt;      unless at.tainted&lt;br /&gt;        send_message("All Tests Passed", "#{output}")&lt;br /&gt;      else&lt;br /&gt;        send_message("Tests Passed", "#{output}")&lt;br /&gt;      end&lt;br /&gt;    end&lt;br /&gt;  end&lt;br /&gt;rescue Exception =&gt; e&lt;br /&gt;end&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;Ctrl+S保存并退出（该脚本暂未支持cucumber的测试结果）。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;4.开始测试之旅&lt;br /&gt;&lt;br /&gt;接下来测试一小段代码开始我们的autotest之旅。&lt;br /&gt;&lt;br /&gt;$ cd ~/code&lt;br /&gt;$ rails rails_autotest&lt;br /&gt;$ cd rails_autotest&lt;br /&gt;$ ./script/generate rspec&lt;br /&gt;$ ./script/autospec (stop by Ctrl+Z)&lt;br /&gt;$ ./script/generate rspec_scaffold post title:string&lt;br /&gt;$ rake db:migrate&lt;br /&gt;$ rake db:test:clone&lt;br /&gt;$ ./script/autospec&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;相关阅读&lt;br /&gt;&lt;br /&gt;&lt;a href="http://chinaonrails.com/topic/view/1075.html"&gt;RSpec, autotest and Snarl on Windows 自动测试完了一声吼&lt;/a&gt;&lt;br /&gt;&lt;a href="http://caffeinatedcode.wordpress.com/2008/02/22/mumble-your-tests/"&gt;mumble your tests&lt;/a&gt; 这篇文章介绍的Mumbles修订版可以为消息提示框加上自定义的图标。&lt;br /&gt;&lt;a href="http://www.agatezone.cn/code/archives/257"&gt;ubuntu 下 rails/rspec/autotest 自动测试环境搭建&lt;/a&gt; 介绍另一种更酷的Rails/autptest方法，更漂亮的效果提示框。推荐！&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/3443179681250049771-4438185155247585298?l=l404.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://l404.blogspot.com/feeds/4438185155247585298/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=3443179681250049771&amp;postID=4438185155247585298' title='2 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/4438185155247585298'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/4438185155247585298'/><link rel='alternate' type='text/html' href='http://l404.blogspot.com/2009/02/ubuntu810rails-autotest.html' title='Ubuntu8.10上小试Rails-AutoTest'/><author><name>404</name><uri>http://www.blogger.com/profile/08660239939199305528</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/_JmYLjCQND0c/SXaZ4wfUELI/AAAAAAAAANg/iLlbANMP1F0/S220/8b1111f6-216a-3853-a396-ee4cff0a219c.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-3443179681250049771.post-3722263442485231419</id><published>2009-01-24T22:40:00.013+08:00</published><updated>2009-02-08T15:53:44.234+08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='Compile'/><category scheme='http://www.blogger.com/atom/ns#' term='Optimize'/><category scheme='http://www.blogger.com/atom/ns#' term='Ruby'/><category scheme='http://www.blogger.com/atom/ns#' term='Memory-Leaks'/><category scheme='http://www.blogger.com/atom/ns#' term='Ubuntu'/><category scheme='http://www.blogger.com/atom/ns#' term='Performance'/><category scheme='http://www.blogger.com/atom/ns#' term='Patch'/><title type='text'>在Ubuntu8.10上编译安装Ruby-1.8.7-p72</title><content type='html'>&lt;span class="Apple-style-span"  style=" ;font-family:verdana;"&gt;1. 卸载系统原apt-get安装过的Ruby及其依赖包（若系统未安装过Ruby可跳过此步骤）&lt;/span&gt;&lt;br /&gt;&lt;span class="Apple-style-span"  style="font-family:verdana;"&gt;&lt;br /&gt;$ sudo apt-get remove --purge ruby&lt;br /&gt;$ sudo apt-get autoremove&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;2. 卸载完毕后，为Ruby构建并安装所需的依赖包&lt;br /&gt;&lt;br /&gt;$ sudo apt-get build-dep ruby1.8&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;3. 下载Ruby源码包并解压&lt;br /&gt;&lt;br /&gt;$ cd ~/downloads/&lt;br /&gt;$ wget http://ftp.ruby-lang.org/pub/ruby/1.8/ruby-1.8.7-p72.tar.gz&lt;br /&gt;$ tar -xvzf ruby-1.8.7-p72.tar.gz&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;4.下载Ruby内存泄漏的补丁包到~/downloads目录，解压后应用补丁&lt;br /&gt;&lt;br /&gt;$ tar -xvzf ~/downloads/MBARIp72patches.tar.gz&lt;br /&gt;$ ~/downloads/MBARIp72patches/apply ~/downloads/ruby-1.8.7-p72&lt;br /&gt;$ cd ruby-1.8.7-p72&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;5. 优化编译选项&lt;br /&gt;&lt;br /&gt;下面export开头的4句针对我本机CPU（基于Intel i686指令集）进行最佳化参数设定，Ruby源码包默认会编译成i486-linux，尽管i686向下兼容，还是物尽其用的好。&lt;br /&gt;&lt;br /&gt;$ export DEB_BUILD_GNU_TYPE=i686-linux-gnu&lt;br /&gt;$ export DEB_BUILD_GNU_CPU=i686&lt;br /&gt;$ export DEB_HOST_GNU_CPU=i686&lt;br /&gt;$ export DEB_HOST_GNU_TYPE=i686-linux-gnu&lt;br /&gt;&lt;br /&gt;CFLAGS的值设置了打上补丁的编译参数，./configure --prefix指定了Ruby的安装目录。如果系统GCC编译器版本较旧，要去掉 -fno-stack-protector；如果软硬件环境都是64位，-mpreferred-stack-boundary=4才行。&lt;br /&gt;&lt;br /&gt;$ CFLAGS="-O2 -fno-stack-protector -mpreferred-stack-boundary=2" ./configure --prefix=$HOME/programs/ruby-1.8.7-p72&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;6. 编译源代码，测试安装可行后执行安装&lt;br /&gt;&lt;br /&gt;$ make&lt;br /&gt;$ make test&lt;br /&gt;$ make install&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;7. 添加到环境变量&lt;br /&gt;&lt;br /&gt;$ gedit ~/.profile&lt;br /&gt;&lt;br /&gt;在~/.profile文件末尾添加如下两句：&lt;br /&gt;&lt;br /&gt;export RUBY_HOME=$HOME/programs/ruby-1.8.7-p72&lt;br /&gt;&lt;br /&gt;export PATH=$RUBY_HOME/bin:$PATH&lt;br /&gt;&lt;br /&gt;注销后重新登录。&lt;br /&gt;&lt;br /&gt;若存在$HOME/bin目录也可如下创建符号链接：&lt;br /&gt;&lt;br /&gt;$ ln -s ~/programs/ruby-1.8.7-p72/bin/* ~/bin&lt;br /&gt;&lt;br /&gt;不推荐这种方式，因为在安装rubygems和其他gem包的时候其可执行文件会在~/programs/ruby-1.8.7-p72/bin目录创建一个副本；与其每次将该副本链接到~/bin目录还不如将~/programs/ruby-1.8.7-p72/bin目录添加到环境变量的查找路径中去。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;8. 检查是否安装成功&lt;br /&gt;&lt;br /&gt;$ ruby -v&lt;br /&gt;&lt;span class="Apple-style-span" style="color: rgb(102, 102, 102);"&gt;ruby 1.8.7 (2009-1-18 MBARI 7/0x4770 on patchlevel 72) [i686-linux]&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;上述输出说明已成功地装上了Ruby并打上了Ruby内存泄漏的补丁包。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;9. 接下来安装Ruby的软件包管理器——rubygems&lt;br /&gt;&lt;br /&gt;$ cd ~/downloads&lt;br /&gt;$ wget http://rubyforge.org/frs/download.php/45905/rubygems-1.3.1.tgz&lt;br /&gt;$ tar -xzvf rubygems-1.3.1.tgz&lt;br /&gt;$ cd rubygems-1.3.1&lt;br /&gt;$ ruby setup.rb&lt;br /&gt;$ gem -v&lt;br /&gt;$ gem install rubygems-update &lt;br /&gt;$ gem update --system&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;10. 清理掉无用目录&lt;br /&gt;&lt;br /&gt;$ rm -rf ~/downloads/ruby-1.8.7-p72&lt;br /&gt;$ rm -rf ~/downloads/MBARIp72patches&lt;br /&gt;$ rm -rf ~/downloads/rubygems-1.3.1&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;相关阅读：&lt;/span&gt;&lt;div&gt;&lt;span class="Apple-style-span"  style="font-family:verdana;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;a href="http://www.javaeye.com/news/4407-ruby-memory-leak-culprit---the-ghost-references-to-stack"&gt;&lt;span class="Apple-style-span"  style="font-family:verdana;"&gt;ruby内存泄漏的罪魁祸首 - 幽灵指针&lt;/span&gt;&lt;/a&gt;&lt;span class="Apple-style-span"  style="font-family:verdana;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;a href="http://www.javaeye.com/news/4592-resolve-memory-leaks-ruby-patch-release"&gt;&lt;span class="Apple-style-span"  style="font-family:verdana;"&gt;解决ruby内存泄漏的超级大补丁发布啦&lt;/span&gt;&lt;/a&gt;&lt;span class="Apple-style-span"  style="font-family:verdana;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;a href="http://www.javaeye.com/topic/307216"&gt;&lt;span class="Apple-style-span"  style="font-family:verdana;"&gt;优化Debian/Ubuntu下的ruby&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;span class="Apple-style-span"  style="font-family:verdana;"&gt;&lt;a href="http://www.javaeye.com/news/5170-ruby-resolve-memory-leak-patch-mbari-has-already-begun-to-be-incorporated-into-the-ruby-source-tree"&gt;解决Ruby内存泄露的补丁MBARI已经开始被合并到Ruby源代码树&lt;/a&gt;&lt;/span&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;span class="Apple-style-span"  style="font-family:verdana;"&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/3443179681250049771-3722263442485231419?l=l404.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://l404.blogspot.com/feeds/3722263442485231419/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=3443179681250049771&amp;postID=3722263442485231419' title='3 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/3722263442485231419'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/3443179681250049771/posts/default/3722263442485231419'/><link rel='alternate' type='text/html' href='http://l404.blogspot.com/2009/01/ubuntu810ruby-187-p72.html' title='在Ubuntu8.10上编译安装Ruby-1.8.7-p72'/><author><name>404</name><uri>http://www.blogger.com/profile/08660239939199305528</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://4.bp.blogspot.com/_JmYLjCQND0c/SXaZ4wfUELI/AAAAAAAAANg/iLlbANMP1F0/S220/8b1111f6-216a-3853-a396-ee4cff0a219c.jpg'/></author><thr:total>3</thr:total></entry></feed>
