# 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) 所示。
圖 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) 顯示提交表單后有一個錯誤。
圖 11.11:包含創建微博表單的首頁圖 11.12:表單中顯示一個錯誤消息的首頁
## 11.3.3 動態流原型
現在創建微博的表單可以使用了,但是用戶看不到實際效果,因為首頁沒有顯示微博。如果你愿意的話,可以在[圖 11.11](#fig-home-with-form) 所示的表單中發表一篇有效的微博,然后打開用戶資料頁面,驗證一下這個表單是否可以正常使用。這樣在頁面之間來來回回有點麻煩,如果能在首頁顯示一個含有當前登入用戶的微博列表(動態流)就好了,構思圖如[圖 11.13](#fig-proto-feed-mockup) 所示。(在[第 12 章](chapter12.html#following-users),我們會在這個微博列表中加入當前登入用戶所關注用戶發表的微博。)
圖 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) 所示。(但是這么做分頁鏈接就失效了,你可以點擊分頁鏈接,看一下是什么原因。)
圖 11.14:顯示有動態流原型的首頁圖 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>
```
圖 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) 所示。
圖 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 練習