11.3 微博相关的操作

微博的数据模型构建好了,也编写了相关的视图文件,接下来我们的开发重点是,通过网页发布微博。本节,我们会初步实现动态流,第 12 章再完善。最后,和用户资源一样,我们还要实现在网页中删除微博的功能。

上述功能的实现和之前的方式有点不同,需要特别注意:微博资源相关的页面不通过微博控制器实现,而是通过资料页面和首页实现。因此微博控制器不需要 newedit 动作,只需要 createdestroy 动作。所以,微博资源的路由如代码清单 11.29 所示。 代码清单 11.29 中的代码对应的 REST 路由如表 11.2 所示,这张表中的路由只是表 2.3 的一部分。不过,路由虽然简化了,但预示着实现的过程需要用到更高级的技术,而不会降低代码的复杂度。从第 2 章起我们就十分依赖脚手架,不过现在我们将舍弃脚手架的大部分功能。

代码清单 11.29:微博资源的路由设置

config/routes.rb

Rails.application.routes.draw do
  root                'static_pages#home'
  get    'help'    => 'static_pages#help'
  get    'about'   => 'static_pages#about'
  get    'contact' => 'static_pages#contact'
  get    'signup'  => 'users#new'
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout'  => 'sessions#destroy'
  resources :users
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
 resources :microposts,          only: [:create, :destroy] end

表 11.2:代码清单 11.29 设置的微博资源路由

HTTP 请求 URL 动作 作用
POST /microposts create 创建新微博
DELETE /microposts/1 destroy 删除 ID 为 1 的微博

11.3.1 访问限制

开发微博资源的第一步,我们要在微博控制器中实现访问限制:若想访问 createdestroy 动作,用户要先登录。

针对这个要求的测试和用户控制器中相应的测试类似(代码清单 9.17代码清单 9.56),我们要使用正确的请求类型访问这两个动作,然后确认微博的数量没有变化,而且会重定向到登录页面,如代码清单 11.30 所示。

代码清单 11.30:微博控制器的访问限制测试 RED

test/controllers/microposts_controller_test.rb

require 'test_helper'

class MicropostsControllerTest < ActionController::TestCase

  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post :create, micropost: { content: "Lorem ipsum" }
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when not logged in" do
    assert_no_difference 'Micropost.count' do
      delete :destroy, id: @micropost
    end
    assert_redirected_to login_url
  end
end

在编写让这个测试通过的应用代码之前,先要做些重构。在 9.2.1 节,我们定义了一个事前过滤器 logged_in_user代码清单 9.12),要求访问相关的动作之前用户要先登录。那时,我们只需要在用户控制器中使用这个事前过滤器,但是现在也要在微博控制器中使用,所以把它移到 ApplicationController 中(所有控制器的基类),如代码清单 11.31 所示。

代码清单 11.31:把 logged_in_user 方法移到 ApplicationController

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper

  private

    # 确保用户已登录
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end

为了避免代码重复,同时还要把用户控制器中的 logged_in_user 方法删掉。

现在,我们可以在微博控制器中使用 logged_in_user 方法了。我们在微博控制器中添加 createdestroy 动作,并使用事前过滤器限制访问,如代码清单 11.32 所示。

代码清单 11.32:限制访问微博控制器的动作 GREEN

app/controllers/microposts_controller.rb

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
  end

  def destroy
  end
end

现在,测试组件应该能通过了:

代码清单 11.33:GREEN
$ bundle exec rake test

11.3.2 创建微博

第 7 章,我们实现了用户注册功能,方法是使用 HTML 表单向用户控制器的 create 动作发送 POST 请求。创建微博的功能实现起来类似,主要的不同点是,表单不放在单独的页面 /microposts/new 中,而是在网站的首页(即根地址 /),构思图如图 11.10 所示。

home page with micropost form mockup bootstrap图 11.10:包含创建微博表单的首页构思图

上一次离开首页时,是图 5.6 那个样子,页面中部有个“Sign up now!”按钮。因为创建微博的表单只对登录后的用户有用,所以本节的目标之一是根据用户的登录状态显示不同的首页内容,如代码清单 11.35 所示。

我们先来编写微博控制器的 create 动作,和用户控制器的 create 动作类似(代码清单 7.23),二者之间主要的区别是,创建微博时,要使用用户和微博的关联关系构建微博对象,如代码清单 11.34 所示。注意 micropost_params 中的健壮参数,只允许通过 Web 修改微博的 content 属性。

代码清单 11.34:微博控制器的 create 动作

app/controllers/microposts_controller.rb

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end

我们使用代码清单 11.35 中的代码编写创建微博所需的表单,这个视图会根据用户的登录状态显示不同的 HTML。

代码清单 11.35:在首页加入创建微博的表单

app/views/static_pages/home.html.erb

<% if logged_in? %>
 <div class="row">
 <aside class="col-md-4">
 <section class="user_info">
  <%= render 'shared/user_info' %>
 </section>
 <section class="micropost_form">
  <%= render 'shared/micropost_form' %>
 </section>
 </aside>
</div> <% else %>
 <div class="center jumbotron">
 <h1>Welcome to the Sample App</h1>

 <h2>
 This is the home page for the
 <a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
 sample application.
 </h2>

  <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
 </div>

  <%= link_to image_tag("rails.png", alt: "Rails logo"),
              'http://rubyonrails.org/' %>
<% end %>

if-else 条件语句中各分支包含的代码太多,有点乱,在练习中会使用局部视图整理。)

为了让代码清单 11.35 能正常渲染页面,我们要创建几个局部视图。首先是首页的侧边栏,如代码清单 11.36 所示。

代码清单 11.36:用户信息侧边栏局部视图

app/views/shared/_user_info.html.erb

<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %></span>
<span><%= pluralize(current_user.microposts.count, "micropost") %></span>

注意,和用户资料页面的侧边栏一样(代码清单 11.23),代码清单 11.36 中的用户信息也显示了用户发布的微博数量。不过显示上有细微的差别,在用户资料页面的侧边栏中,“Microposts” 是“标注”(label),所以“Microposts (1)”这样的用法是合理的。而在本例中,如果说“1 microposts”的话就不合语法了,所以我们调用了 pluralize 方法(7.3.3 节见过),显示成“1 micropost”,“2 microposts”等。

下面我们来编写微博创建表单的局部视图,如代码清单 11.37 所示。这段代码和代码清单 7.13 中的注册表单类似。

代码清单 11.37:微博创建表单局部视图

app/views/shared/_micropost_form.html.erb

<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
 <div class="field">
  <%= f.text_area :content, placeholder: "Compose new micropost..." %>
 </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>

我们还要做两件事,代码清单 11.37 中的表单才能使用。第一,(和之前一样)我们要通过关联定义 @micropost 变量:

@micropost = current_user.microposts.build

把这行代码写入控制器,如代码清单 11.38 所示。

代码清单 11.38:在 home 动作中定义 @micropost 实例变量

app/controllers/static_pages_controller.rb

class StaticPagesController < ApplicationController

  def home
 @micropost = current_user.microposts.build if logged_in?  end

  def help
  end

  def about
  end

  def contact
  end
end

因为只有用户登录后 current_user 才存在,所以 @micropost 变量只能在用户登录后再定义。

我们要做的第二件事是,重写错误消息局部视图,让代码清单 11.37 中的这行能用:

<%= render 'shared/error_messages', object: f.object %>

你可能还记得,在代码清单 7.18 中,错误消息局部视图直接引用了 @user 变量,但现在我们提供的变量是 @micropost。为了在两个地方都能使用这个错误消息局部视图,我们可以把表单变量 f 传入局部视图,通过 f.object 获取相应的对象。因此,在 form_for(@user) do |f| 中,f.object@user;在 form_for(@micropost) do |f| 中,f.object@micropost

我们要通过一个哈希把对象传入局部视图,值是这个对象,键是局部视图中所需的变量名,如代码清单 11.37 中的第二行所示。换句话说,object: f.object 会创建一个名为 object 的变量,供 error_messages 局部视图使用。通过这个对象,我们可以定制错误消息,如代码清单 11.39 所示。

代码清单 11.39:能使用其他对象的错误消息局部视图 RED

app/views/shared/_error_messages.html.erb

<% if object.errors.any? %>
 <div id="error_explanation">
 <div class="alert alert-danger">
 The form contains <%= pluralize(object.errors.count, "error") %>.
 </div>
 <ul>
  <% object.errors.full_messages.each do |msg| %>
 <li><%= msg %></li>
  <% end %>
 </ul>
 </div>
<% end %>

现在,你应该确认一下测试组件无法通过:

代码清单 11.40:RED
$ bundle exec rake test

这提醒我们要修改其他使用错误消息局部视图的视图,包括用户注册视图(代码清单 7.18),重设密码视图(代码清单 10.50)和编辑用户视图(代码清单 9.2)。这三个视图修改后的版本分别如代码清单 11.41代码清单 11.43代码清单 11.42 所示。

代码清单 11.41:修改用户注册表单中渲染错误消息局部视图的方式

app/views/users/new.html.erb

<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
 <div class="col-md-6 col-md-offset-3">
  <%= form_for(@user) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <%= f.label :name %>
  <%= f.text_field :name, class: 'form-control' %>

  <%= f.label :email %>
  <%= f.email_field :email, class: 'form-control' %>

  <%= f.label :password %>
  <%= f.password_field :password, class: 'form-control' %>

  <%= f.label :password_confirmation, "Confirmation" %>
  <%= f.password_field :password_confirmation, class: 'form-control' %>

  <%= f.submit "Create my account", class: "btn btn-primary" %>
  <% end %>
 </div>
</div>
代码清单 11.42:修改编辑用户表单中渲染错误消息局部视图的方式

app/views/users/edit.html.erb

<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
 <div class="col-md-6 col-md-offset-3">
  <%= form_for(@user) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
  <%= f.label :name %>
  <%= f.text_field :name, class: 'form-control' %>

  <%= f.label :email %>
  <%= f.email_field :email, class: 'form-control' %>

  <%= f.label :password %>
  <%= f.password_field :password, class: 'form-control' %>

  <%= f.label :password_confirmation, "Confirmation" %>
  <%= f.password_field :password_confirmation, class: 'form-control' %>

  <%= f.submit "Save changes", class: "btn btn-primary" %>
  <% end %>

 <div class="gravatar_edit">
  <%= gravatar_for @user %>
 <a href="http://gravatar.com/emails">change</a>
 </div>
 </div>
</div>
代码清单 11.43:修改密码重设表单中渲染错误消息局部视图的方式

app/views/password_resets/edit.html.erb

<% provide(:title, 'Reset password') %>
<h1>Password reset</h1>

<div class="row">
 <div class="col-md-6 col-md-offset-3">
  <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
  <%= hidden_field_tag :email, @user.email %>

  <%= f.label :password %>
  <%= f.password_field :password, class: 'form-control' %>

  <%= f.label :password_confirmation, "Confirmation" %>
  <%= f.password_field :password_confirmation, class: 'form-control' %>

  <%= f.submit "Update password", class: "btn btn-primary" %>
  <% end %>
 </div>
</div>

现在,所有测试应该都能通过了:

$ bundle exec rake test

而且,本节添加的所有 HTML 代码也都能正确渲染了。图 11.11 是创建微博的表单,图 11.12 显示提交表单后有一个错误。

home with form 3rd edition图 11.11:包含创建微博表单的首页home form errors 3rd edition图 11.12:表单中显示一个错误消息的首页

11.3.3 动态流原型

现在创建微博的表单可以使用了,但是用户看不到实际效果,因为首页没有显示微博。如果你愿意的话,可以在图 11.11 所示的表单中发表一篇有效的微博,然后打开用户资料页面,验证一下这个表单是否可以正常使用。这样在页面之间来来回回有点麻烦,如果能在首页显示一个含有当前登入用户的微博列表(动态流)就好了,构思图如图 11.13 所示。(在第 12 章,我们会在这个微博列表中加入当前登入用户所关注用户发表的微博。)

proto feed mockup 3rd edition图 11.13:显示有动态流的首页构思图

因为每个用户都有一个动态流,因此我们可以在用户模型中定义一个名为 feed 的方法,查找当前用户发表的所有微博。我们要在微博模型上调用 where 方法(10.5 节提到过)查找微博,如代码清单 11.44 所示。[6]

代码清单 11.44:微博动态流的初步实现

app/models/user.rb

class User < ActiveRecord::Base
  .
  .
  .
  # 实现动态流原型
  # 完整的实现参见第 12 章
  def feed
 Micropost.where("user_id = ?", id)  end

    private
    .
    .
    .
end

Micropost.where("user_id = ?", id) 中的问号确保 id 的值在传入底层的 SQL 查询语句之前做了适当的转义,避免“SQL 注入”(SQL injection)这种严重的安全隐患。这里用到的 id 属性是个整数,没什么危险,不过在 SQL 语句中引入变量之前做转义是个好习惯。

细心的读者可能已经注意到了,代码清单 11.44 中的代码和下面的代码是等效的:

def feed
  microposts
end

我们之所以使用代码清单 11.44 中的版本,是因为它能更好的服务于第 12 章实现的完整动态流。

要在演示应用中添加动态流,我们可以在 home 动作中定义一个 @feed_items 实例变量,分页获取当前用户的微博,如代码清单 11.45 所示。然后在首页(参见代码清单 11.47)中加入一个动态流局部视图(参见代码清单 11.46)。注意,现在用户登录后要执行两行代码,所以代码清单 11.45代码清单 11.38 中的

@micropost = current_user.microposts.build if logged_in?

改成了

if logged_in?
  @micropost  = current_user.microposts.build
  @feed_items = current_user.feed.paginate(page: params[:page])
end

也就是把条件放在行尾的代码改成了使用 if-end 语句。

代码清单 11.45:在 home 动作中定义一个实例变量,获取动态流

app/controllers/static_pages_controller.rb

class StaticPagesController < ApplicationController

  def home
    if logged_in?
      @micropost  = current_user.microposts.build
 @feed_items = current_user.feed.paginate(page: params[:page])    end
  end

  def help
  end

  def about
  end

  def contact
  end
end
代码清单 11.46:动态流局部视图

app/views/shared/_feed.html.erb

<% if @feed_items.any? %>
 <ol class="microposts">
  <%= render @feed_items %>
 </ol>
  <%= will_paginate @feed_items %>
<% end %>

动态流局部视图使用如下的代码,把单篇微博交给代码清单 11.21 中的局部视图渲染:

<%= render @feed_items %>

Rails 知道要渲染 micropost 局部视图,因为 @feed_items 中的元素都是 Micropost 类的实例。所以,Rails 会在对应资源的视图文件夹中寻找正确的局部视图:

app/views/microposts/_micropost.html.erb

和之前一样,我们可以把动态流局部视图加入首页,如代码清单 11.47 所示。加入后的效果就是在首页显示动态流,实现了我们的需求,如图 11.14 所示。

代码清单 11.47:在首页加入动态流

app/views/static_pages/home.html.erb

<% if logged_in? %>
 <div class="row">
 <aside class="col-md-4">
 <section class="user_info">
  <%= render 'shared/user_info' %>
 </section>
 <section class="micropost_form">
  <%= render 'shared/micropost_form' %>
 </section>
 </aside>
 <div class="col-md-8">
 <h3>Micropost Feed</h3>
  <%= render 'shared/feed' %>
 </div>
 </div>
<% else %>
 .
 .
 .
<% end %>

现在,发布新微博的功能可以按照设想的方式使用了,如图 11.15 所示。不过还有个小小的不足:如果发布微博失败,首页还会需要一个名为 @feed_items 的实例变量,所以提交失败时网站无法正常运行。最简单的解决方法是,如果提交失败就把 @feed_items 设为空数组,如代码清单 11.48 所示。(但是这么做分页链接就失效了,你可以点击分页链接,看一下是什么原因。)

home with proto feed 3rd edition图 11.14:显示有动态流原型的首页micropost created 3rd edition图 11.15:发布新微博后的首页

代码清单 11.48:在 create 动作中定义 @feed_items 实例变量,值为空数组

app/controllers/microposts_controller.rb

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
 @feed_items = []      render 'static_pages/home'
    end
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end

11.3.4 删除微博

我们要为微博资源实现的最后一个功能是删除。和删除用户类似(9.4.2 节),删除微博也要通过删除链接实现,构思图如图 11.16 所示。用户只有管理员才能删除,而微博只有发布人才能删除。

首先,我们要在微博局部视图(代码清单 11.21)中加入删除链接,如代码清单 11.49 所示。

代码清单 11.49:在微博局部视图中添加删除链接

app/views/microposts/_micropost.html.erb

<li id="<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
 <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
 <span class="content"><%= micropost.content %></span>
 <span class="timestamp">
 Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  <% if current_user?(micropost.user) %>
  <%= link_to "delete", micropost, method: :delete,
 data: { confirm: "You sure?" } %>  <% end %>
 </span>
</li>

micropost delete links mockup 3rd edition图 11.16:显示有删除链接的动态流原型构思图

然后,参照 UsersControllerdestroy 动作(代码清单 9.54),编写 MicropostsControllerdestroy 动作。在 UsersController 中,我们在 admin_user 事前过滤器中定义 @user 变量,查找用户,但现在要通过关联查找微博,这么做,如果某个用户试图删除其他用户的微博,会自动失败。我们把查找微博的操作放在 correct_user 事前过滤器中,确保当前用户确实拥有指定 ID 的微博,如代码清单 11.50 所示。

代码清单 11.50:MicropostsControllerdestroy 动作

app/controllers/microposts_controller.rb

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
 before_action :correct_user,   only: :destroy  .
  .
  .
  def destroy
 @micropost.destroy flash[:success] = "Micropost deleted" redirect_to request.referrer || root_url  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end

    def correct_user
 @micropost = current_user.microposts.find_by(id: params[:id]) redirect_to root_url if @micropost.nil?    end
end

注意,在 destroy 动作中重定向的地址是:

request.referrer || root_url

request.referrer [7] 和实现友好转向时使用的 request.url 关系紧密,表示前一个 URL(这里是首页)。[8]因为首页和资料页面都有微博,所以这么做很方便,我们使用 request.referrer 把用户重定向到发起删除请求的页面,如果 request.referrernil(例如在某些测试中),就转向 root_url。(可以和代码清单 8.50 中设置参数默认值的用法对比一下。)

添加上述代码后,删除最新发布的第二篇微博后显示的页面如图 11.17 所示。

home post delete 3rd edition图 11.17:删除最新发布的第二篇微博后显示的首页

11.3.5 微博的测试

至此,微博模型和相关的界面完成了。我们还要编写简短的微博控制器测试,检查权限限制,以及一个集成测试,检查整个操作流程。

首先,在微博固件中添加一些由不同用户发布的微博,如代码清单 11.51 所示。(现在只需要使用一个微博固件,但还是要多添加几个,以备后用。)

代码清单 11.51:添加几个由不同用户发布的微博

test/fixtures/microposts.yml

.
.
.
ants:
  content: "Oh,  is  that  what  you  want?  Because  that's  how  you  get  ants!"
  created_at: <%= 2.years.ago %>
  user: archer

zone:
  content: "Danger  zone!"
  created_at: <%= 3.days.ago %>
  user: archer

tone:
  content: "I'm  sorry.  Your  words  made  sense,  but  your  sarcastic  tone  did  not."
  created_at: <%= 10.minutes.ago %>
  user: lana

van:
  content: "Dude,  this  van's,  like,  rolling  probable  cause."
  created_at: <%= 4.hours.ago %>
  user: lana

然后,编写一个简短的测试,确保某个用户不能删除其他用户的微博,并且要重定向到正确的地址,如代码清单 11.52 所示。

代码清单 11.52:测试用户不能删除其他用户的微博 GREEN

test/controllers/microposts_controller_test.rb

require 'test_helper'

class MicropostsControllerTest < ActionController::TestCase

  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post :create, micropost: { content: "Lorem ipsum" }
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when not logged in" do
    assert_no_difference 'Micropost.count' do
      delete :destroy, id: @micropost
    end
    assert_redirected_to login_url
  end

 test "should redirect destroy for wrong micropost" do log_in_as(users(:michael)) micropost = microposts(:ants) assert_no_difference 'Micropost.count' do delete :destroy, id: micropost end assert_redirected_to root_url end end

最后,编写一个集成测试:登录,检查有没有分页链接,然后分别提交有效和无效的微博,再删除一篇微博,最后访问另一个用户的资料页面,确保没有删除链接。和之前一样,使用下面的命令生成测试文件:

$ rails generate integration_test microposts_interface
      invoke  test_unit
      create    test/integration/microposts_interface_test.rb

这个测试的代码如代码清单 11.53 所示。看看你能否把代码和前面说的步骤对应起来。(在这个测试中,post 请求后调用了 follow_redirect!,而没有直接使用 post_via_redirect,这是要兼顾代码清单 11.68 中的图片上传测试。)

代码清单 11.53:微博资源界面的集成测试 GREEN

test/integration/microposts_interface_test.rb

require 'test_helper'

class MicropostInterfaceTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    # 无效提交
    assert_no_difference 'Micropost.count' do
      post microposts_path, micropost: { content: "" }
    end
    assert_select 'div#error_explanation'
    # 有效提交
    content = "This micropost really ties the room together"
    assert_difference 'Micropost.count', 1 do
      post microposts_path, micropost: { content: content }
    end
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    # 删除一篇微博
    assert_select 'a', text: 'delete'
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(first_micropost)
    end
    # 访问另一个用户的资料页面
    get user_path(users(:archer))
    assert_select 'a', text: 'delete', count: 0
  end
end

因为我们已经把可以正常运行的应用开发好了,所以测试组件应该可以通过:

代码清单 11.54:GREEN
$ bundle exec rake test