# 11.3 微博相关的操作
微博的数据模型构建好了,也编写了相关的视图文件,接下来我们的开发重点是,通过网页发布微博。本节,我们会初步实现动态流,[第 12 章](chapter12.html#following-users)再完善。最后,和用户资源一样,我们还要实现在网页中删除微博的功能。
上述功能的实现和之前的方式有点不同,需要特别注意:微博资源相关的页面不通过微博控制器实现,而是通过资料页面和首页实现。因此微博控制器不需要 `new` 和 `edit` 动作,只需要 `create` 和 `destroy` 动作。所以,微博资源的路由如[代码清单 11.29](#listing-microposts-resource) 所示。 [代码清单 11.29](#listing-microposts-resource) 中的代码对应的 REST 路由如[表 11.2](#table-restful-microposts) 所示,这张表中的路由只是[表 2.3](chapter2.html#table-demo-restful-microposts) 的一部分。不过,路由虽然简化了,但预示着实现的过程需要用到更高级的技术,而不会降低代码的复杂度。从[第 2 章](chapter2.html#a-toy-app)起我们就十分依赖脚手架,不过现在我们将舍弃脚手架的大部分功能。
##### 代码清单 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](#listing-microposts-resource) 设置的微博资源路由
| HTTP 请求 | URL | 动作 | 作用 |
| --- | --- | --- | --- |
| `POST` | /microposts | `create` | 创建新微博 |
| `DELETE` | /microposts/1 | `destroy` | 删除 ID 为 1 的微博 |
## 11.3.1 访问限制
开发微博资源的第一步,我们要在微博控制器中实现访问限制:若想访问 `create` 和 `destroy` 动作,用户要先登录。
针对这个要求的测试和用户控制器中相应的测试类似([代码清单 9.17](chapter9.html#listing-edit-update-redirect-tests) 和[代码清单 9.56](chapter9.html#listing-action-tests-admin)),我们要使用正确的请求类型访问这两个动作,然后确认微博的数量没有变化,而且会重定向到登录页面,如[代码清单 11.30](#listing-create-destroy-micropost-tests) 所示。
##### 代码清单 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 节](chapter9.html#requiring-logged-in-users),我们定义了一个事前过滤器 `logged_in_user`([代码清单 9.12](chapter9.html#listing-authorize-before-filter)),要求访问相关的动作之前用户要先登录。那时,我们只需要在用户控制器中使用这个事前过滤器,但是现在也要在微博控制器中使用,所以把它移到 `ApplicationController` 中(所有控制器的基类),如[代码清单 11.31](#listing-sessions-helper-authenticate) 所示。
##### 代码清单 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` 方法了。我们在微博控制器中添加 `create` 和 `destroy` 动作,并使用事前过滤器限制访问,如[代码清单 11.32](#listing-microposts-controller-access-control) 所示。
##### 代码清单 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 章](chapter7.html#sign-up),我们实现了用户注册功能,方法是使用 HTML 表单向用户控制器的 `create` 动作发送 `POST` 请求。创建微博的功能实现起来类似,主要的不同点是,表单不放在单独的页面 /microposts/new 中,而是在网站的首页(即根地址 /),构思图如[图 11.10](#fig-home-page-with-micropost-form-mockup) 所示。
![home page with micropost form mockup bootstrap](https://box.kancloud.cn/2016-05-11_57333072d4a5f.png)图 11.10:包含创建微博表单的首页构思图
上一次离开首页时,是[图 5.6](chapter5.html#fig-sample-app-logo) 那个样子,页面中部有个“Sign up now!”按钮。因为创建微博的表单只对登录后的用户有用,所以本节的目标之一是根据用户的登录状态显示不同的首页内容,如[代码清单 11.35](#listing-microposts-home-page) 所示。
我们先来编写微博控制器的 `create` 动作,和用户控制器的 `create` 动作类似([代码清单 7.23](chapter7.html#listing-user-create-action)),二者之间主要的区别是,创建微博时,要使用用户和微博的关联关系构建微博对象,如[代码清单 11.34](#listing-microposts-create-action) 所示。注意 `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](#listing-microposts-home-page) 中的代码编写创建微博所需的表单,这个视图会根据用户的登录状态显示不同的 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` 条件语句中各分支包含的代码太多,有点乱,在[练习](#user-microposts-exercises)中会使用局部视图整理。)
为了让[代码清单 11.35](#listing-microposts-home-page) 能正常渲染页面,我们要创建几个局部视图。首先是首页的侧边栏,如[代码清单 11.36](#listing-user-info) 所示。
##### 代码清单 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](#listing-user-show-microposts)),[代码清单 11.36](#listing-user-info) 中的用户信息也显示了用户发布的微博数量。不过显示上有细微的差别,在用户资料页面的侧边栏中,“Microposts” 是“标注”(label),所以“Microposts (1)”这样的用法是合理的。而在本例中,如果说“1 microposts”的话就不合语法了,所以我们调用了 `pluralize` 方法([7.3.3 节](chapter7.html#signup-error-messages)见过),显示成“1 micropost”,“2 microposts”等。
下面我们来编写微博创建表单的局部视图,如[代码清单 11.37](#listing-micropost-form) 所示。这段代码和[代码清单 7.13](chapter7.html#listing-signup-form) 中的注册表单类似。
##### 代码清单 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](#listing-micropost-form) 中的表单才能使用。第一,(和之前一样)我们要通过关联定义 `@micropost` 变量:
```
@micropost = current_user.microposts.build
```
把这行代码写入控制器,如[代码清单 11.38](#listing-micropost-instance-variable) 所示。
##### 代码清单 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](#listing-micropost-form) 中的这行能用:
```
<%= render 'shared/error_messages', object: f.object %>
```
你可能还记得,在[代码清单 7.18](chapter7.html#listing-f-error-messages) 中,错误消息局部视图直接引用了 `@user` 变量,但现在我们提供的变量是 `@micropost`。为了在两个地方都能使用这个错误消息局部视图,我们可以把表单变量 `f` 传入局部视图,通过 `f.object` 获取相应的对象。因此,在 `form_for(@user) do |f|` 中,`f.object` 是 `@user`;在 `form_for(@micropost) do |f|` 中,`f.object` 是 `@micropost`。
我们要通过一个哈希把对象传入局部视图,值是这个对象,键是局部视图中所需的变量名,如[代码清单 11.37](#listing-micropost-form) 中的第二行所示。换句话说,`object: f.object` 会创建一个名为 `object` 的变量,供 `error_messages` 局部视图使用。通过这个对象,我们可以定制错误消息,如[代码清单 11.39](#listing-updated-error-messages-partial) 所示。
##### 代码清单 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](chapter7.html#listing-f-error-messages)),重设密码视图([代码清单 10.50](chapter10.html#listing-password-reset-form))和编辑用户视图([代码清单 9.2](chapter9.html#listing-user-edit-view))。这三个视图修改后的版本分别如[代码清单 11.41](#listing-signup-errors-updated),[代码清单 11.43](#listing-password-reset-updated) 和[代码清单 11.42](#listing-edit-errors-updated) 所示。
##### 代码清单 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](#fig-home-with-form) 是创建微博的表单,[图 11.12](#fig-home-form-errors) 显示提交表单后有一个错误。
![home with form 3rd edition](https://box.kancloud.cn/2016-05-11_5733307305313.png)图 11.11:包含创建微博表单的首页![home form errors 3rd edition](https://box.kancloud.cn/2016-05-11_5733307320ad9.png)图 11.12:表单中显示一个错误消息的首页
## 11.3.3 动态流原型
现在创建微博的表单可以使用了,但是用户看不到实际效果,因为首页没有显示微博。如果你愿意的话,可以在[图 11.11](#fig-home-with-form) 所示的表单中发表一篇有效的微博,然后打开用户资料页面,验证一下这个表单是否可以正常使用。这样在页面之间来来回回有点麻烦,如果能在首页显示一个含有当前登入用户的微博列表(动态流)就好了,构思图如[图 11.13](#fig-proto-feed-mockup) 所示。(在[第 12 章](chapter12.html#following-users),我们会在这个微博列表中加入当前登入用户所关注用户发表的微博。)
![proto feed mockup 3rd edition](https://box.kancloud.cn/2016-05-11_5733307336beb.png)图 11.13:显示有动态流的首页构思图
因为每个用户都有一个动态流,因此我们可以在用户模型中定义一个名为 `feed` 的方法,查找当前用户发表的所有微博。我们要在微博模型上调用 `where` 方法([10.5 节](chapter10.html#account-activation-and-password-reset-exercises)提到过)查找微博,如[代码清单 11.44](#listing-proto-status-feed) 所示。[[6](#fn-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 注入](http://en.wikipedia.org/wiki/SQL_injection)”(SQL injection)这种严重的安全隐患。这里用到的 `id` 属性是个整数,没什么危险,不过在 SQL 语句中引入变量之前做转义是个好习惯。
细心的读者可能已经注意到了,[代码清单 11.44](#listing-proto-status-feed) 中的代码和下面的代码是等效的:
```
def feed
microposts
end
```
我们之所以使用[代码清单 11.44](#listing-proto-status-feed) 中的版本,是因为它能更好的服务于[第 12 章](chapter12.html#following-users)实现的完整动态流。
要在演示应用中添加动态流,我们可以在 `home` 动作中定义一个 `@feed_items` 实例变量,分页获取当前用户的微博,如[代码清单 11.45](#listing-feed-instance-variable) 所示。然后在首页(参见[代码清单 11.47](#listing-home-with-feed))中加入一个动态流局部视图(参见[代码清单 11.46](#listing-feed-partial))。注意,现在用户登录后要执行两行代码,所以[代码清单 11.45](#listing-feed-instance-variable) 把[代码清单 11.38](#listing-micropost-instance-variable) 中的
```
@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](#listing-micropost-partial) 中的局部视图渲染:
```
<%= render @feed_items %>
```
Rails 知道要渲染 `micropost` 局部视图,因为 `@feed_items` 中的元素都是 `Micropost` 类的实例。所以,Rails 会在对应资源的视图文件夹中寻找正确的局部视图:
```
app/views/microposts/_micropost.html.erb
```
和之前一样,我们可以把动态流局部视图加入首页,如[代码清单 11.47](#listing-home-with-feed) 所示。加入后的效果就是在首页显示动态流,实现了我们的需求,如[图 11.14](#fig-home-with-proto-feed) 所示。
##### 代码清单 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](#fig-micropost-created) 所示。不过还有个小小的不足:如果发布微博失败,首页还会需要一个名为 `@feed_items` 的实例变量,所以提交失败时网站无法正常运行。最简单的解决方法是,如果提交失败就把 `@feed_items` 设为空数组,如[代码清单 11.48](#listing-microposts-create-action-with-feed) 所示。(但是这么做分页链接就失效了,你可以点击分页链接,看一下是什么原因。)
![home with proto feed 3rd edition](https://box.kancloud.cn/2016-05-11_573330735711f.png)图 11.14:显示有动态流原型的首页![micropost created 3rd edition](https://box.kancloud.cn/2016-05-11_573330739b11e.png)图 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 节](chapter9.html#the-destroy-action)),删除微博也要通过删除链接实现,构思图如[图 11.16](#fig-micropost-delete-links-mockup) 所示。用户只有管理员才能删除,而微博只有发布人才能删除。
首先,我们要在微博局部视图([代码清单 11.21](#listing-micropost-partial))中加入删除链接,如[代码清单 11.49](#listing-micropost-partial-with-delete) 所示。
##### 代码清单 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](https://box.kancloud.cn/2016-05-11_57333073b9e53.png)图 11.16:显示有删除链接的动态流原型构思图
然后,参照 `UsersController` 的 `destroy` 动作([代码清单 9.54](chapter9.html#listing-admin-destroy-before-filter)),编写 `MicropostsController` 的 `destroy` 动作。在 `UsersController` 中,我们在 `admin_user` 事前过滤器中定义 `@user` 变量,查找用户,但现在要通过关联查找微博,这么做,如果某个用户试图删除其他用户的微博,会自动失败。我们把查找微博的操作放在 `correct_user` 事前过滤器中,确保当前用户确实拥有指定 ID 的微博,如[代码清单 11.50](#listing-microposts-destroy-action) 所示。
##### 代码清单 11.50:`MicropostsController` 的 `destroy` 动作
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](#fn-7)] 和实现友好转向时使用的 `request.url` 关系紧密,表示前一个 URL(这里是首页)。[[8](#fn-8)]因为首页和资料页面都有微博,所以这么做很方便,我们使用 `request.referrer` 把用户重定向到发起删除请求的页面,如果 `request.referrer` 为 `nil`(例如在某些测试中),就转向 `root_url`。(可以和[代码清单 8.50](chapter8.html#listing-test-helper-log-in) 中设置参数默认值的用法对比一下。)
添加上述代码后,删除最新发布的第二篇微博后显示的页面如[图 11.17](#fig-home-post-delete) 所示。
![home post delete 3rd edition](https://box.kancloud.cn/2016-05-11_57333073d7635.png)图 11.17:删除最新发布的第二篇微博后显示的首页
## 11.3.5 微博的测试
至此,微博模型和相关的界面完成了。我们还要编写简短的微博控制器测试,检查权限限制,以及一个集成测试,检查整个操作流程。
首先,在微博固件中添加一些由不同用户发布的微博,如[代码清单 11.51](#listing-add-micropost-different-owner) 所示。(现在只需要使用一个微博固件,但还是要多添加几个,以备后用。)
##### 代码清单 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](#listing-micropost-user-mismatch-test) 所示。
##### 代码清单 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](#listing-microposts-interface-test) 所示。看看你能否把代码和前面说的步骤对应起来。(在这个测试中,`post` 请求后调用了 `follow_redirect!`,而没有直接使用 `post_via_redirect`,这是要兼顾[代码清单 11.68](#listing-image-upload-test) 中的图片上传测试。)
##### 代码清单 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
```
- Ruby on Rails 教程
- 致中国读者
- 序
- 致谢
- 作者译者简介
- 版权和代码授权协议
- 第 1 章 从零开始,完成一次部署
- 1.1 简介
- 1.2 搭建环境
- 1.3 第一个应用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小结
- 1.7 练习
- 第 2 章 玩具应用
- 2.1 规划应用
- 2.2 用户资源
- 2.3 微博资源
- 2.4 小结
- 2.5 练习
- 第 3 章 基本静态的页面
- 3.1 创建演示应用
- 3.2 静态页面
- 3.3 开始测试
- 3.4 有点动态内容的页面
- 3.5 小结
- 3.6 练习
- 3.7 高级测试技术
- 第 4 章 Rails 背后的 Ruby
- 4.1 导言
- 4.2 字符串和方法
- 4.3 其他数据类型
- 4.4 Ruby 类
- 4.5 小结
- 4.6 练习
- 第 5 章 完善布局
- 5.1 添加一些结构
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的链接
- 5.4 用户注册:第一步
- 5.5 小结
- 5.6 练习
- 第 6 章 用户模型
- 6.1 用户模型
- 6.2 用户数据验证
- 6.3 添加安全密码
- 6.4 小结
- 6.5 练习
- 第 7 章 注册
- 7.1 显示用户的信息
- 7.2 注册表单
- 7.3 注册失败
- 7.4 注册成功
- 7.5 专业部署方案
- 7.6 小结
- 7.7 练习
- 第 8 章 登录和退出
- 8.1 会话
- 8.2 登录
- 8.3 退出
- 8.4 记住我
- 8.5 小结
- 8.6 练习
- 第 9 章 更新,显示和删除用户
- 9.1 更新用户
- 9.2 权限系统
- 9.3 列出所有用户
- 9.4 删除用户
- 9.5 小结
- 9.6 练习
- 第 10 章 账户激活和密码重设
- 10.1 账户激活
- 10.2 密码重设
- 10.3 在生产环境中发送邮件
- 10.4 小结
- 10.5 练习
- 10.6 证明超时失效的比较算式
- 第 11 章 用户的微博
- 11.1 微博模型
- 11.2 显示微博
- 11.3 微博相关的操作
- 11.4 微博中的图片
- 11.5 小结
- 11.6 练习
- 第 12 章 关注用户
- 12.1 “关系”模型
- 12.2 关注用户的网页界面
- 12.3 动态流
- 12.4 小结
- 12.5 练习