Controller Spec
Rails 5 移除了 Controller 測試!,但可以使用這個 rails-controller-testing RubyGem 繼續沿用。
關於 Rails Controller 請參考這篇文章 Action Controller Overview。
命名慣例
文件擺放位置:
spec/controllers/application_controller.rb
一樣按照 Controller 的慣例複數_controller
。
spec/controllers/users_controller.rb
若是 Admin::UsersController
,則:
spec/controllers/admin/users_controller.rb
。
若文件按照命名慣例,放在 spec/controllers
下,RSpec 就會自動幫我們推測出,這個檔案是一個 Controller spec。會自動幫我們加上 type: :controller
這個 metadata 到我們第一層的 describe
區塊。
原本要寫成這樣:
RSpec.describe UsersController, type: :controller do
end
現在放對位置只要寫成
RSpec.describe UsersController do
end
即可!
RESTful actions
七個 RESTful actions 按順序通常是:index
, new
, create
, show
, edit
, update
, destroy
。
通常按正常流程實作一套 CRUD 便會是這個順序。
Controller 通常會測 response 是否成功,便可以知道該頁可不可以存取。測試 Redirect 則可以了解,譬如使用者刷卡後,是否成功跳轉到我們想要使用者去的頁面。測試 HTTP 狀態,則可以確定出錯時,是不是返回正確的 HTTP 狀態碼。再進一步也可以測試該頁的 Flash 是否如預期般顯示。
HTTP 動詞
有五個要了解的動詞,GET、POST、PUT、PATCH、DELETE,HTTP 動詞通常都是採取全大寫的寫法。
RESTful 的七個 actions,有些 path 是重複的,唯一區別就在於 HTTP 動詞不一樣。
譬如一個 PhotosController
的 index
跟 create
action,他們的目標路徑是一樣的,都是 /photos
。不一樣的點在於,index
用 GET,而 create
用 POST。
- 獲得資料:GET
- 發送資料:POST
- 更新資料:PUT、PATCH
- 刪除資料:DELETE
不太了解可以再閱讀一次 Rails Routing from the Outside In再複習一下。
使用 HTTP 動詞我們說「請求」,伺服器處理完會回復「響應」給我們。
- request 請求
- response 響應
Controller 要測什麼
測試 Controller 的所有功能都是由 Rails 內建的 ActionController::TestCase::Behavior
這個類別所提供,RSpec 對 ActionController::TestCase::Behavior
進行了包裝,提供我們幾個好用的方法與物件:
response
代表「響應」的物件
response
,中文稱響應,是屬於 ActionController::TestResponse
類別的物件,代表上一次的 HTTP 響應。譬如在 Controller 測試裡,撰寫 get :index
,便可以向 Controller 的 index
action 發送 GET 請求:
class UsersController < ApplicationController
def index
end
end
RSpec.describe UsersController do
describe "#index" do
it "response is success" do
get :index
expect(response.success?).to eq true
end
end
end
response
有一個實體方法 success?
,HTTP 請求成功響應(狀態碼為 20x),則為 true
,反之為 false
。
expect(response.success?).to eq true
通常會寫成這樣:
expect(response).to be_success
這是 RSpec 的 一種慣用寫法。
render_template
模版的算繪redirect_to
轉址have_http_status
HTTP 狀態
RSpec.describe UsersController do
describe "#index" do
it "response is success" do
get :index
expect(response).to render_template(:index)
expect(response).to have_http_status(200)
end
end
end
多種不同的發請求形式
那往 create
發 POST 呢?
post :create
就是往 Controller
的 create
action 發出 POST 請求。也可以用字串:
post "create"
但是寫 Symbol
可以少打一個字元,也不會配置額外的記憶體!
也可以指定往一個路徑發送特定 HTTP 動詞的請求:
post "/photos"
或是使用路由的幫助方法(Routing Helpers):
post photos_path
相對路徑
post photos_url
或是絕對路徑都可以!
若是要向指定路徑發送請求,有時候需求變了,路由會一併做更改,所以用路由的幫助方法(Routing Helpers)來引用路徑比較好,這樣路由變了也不用回來修改測試發送的路徑。
測試 Response
expect(response).to be_success
expect(response).not_to be_success
測試 HTTP 狀態
expect(response).to have_http_status(:unauthorized)
狀態列表
100 :continue
101 :switching_protocols
102 :processing
200 :ok
201 :created
202 :accepted
203 :non_authoritative_information
204 :no_content
205 :reset_content
206 :partial_content
207 :multi_status
208 :already_reported
226 :im_used
300 :multiple_choices
301 :moved_permanently
302 :found
303 :see_other
304 :not_modified
305 :use_proxy
307 :temporary_redirect
308 :permanent_redirect
400 :bad_request
401 :unauthorized
402 :payment_required
403 :forbidden
404 :not_found
405 :method_not_allowed
406 :not_acceptable
407 :proxy_authentication_required
408 :request_timeout
409 :conflict
410 :gone
411 :length_required
412 :precondition_failed
413 :payload_too_large
414 :uri_too_long
415 :unsupported_media_type
416 :range_not_satisfiable
417 :expectation_failed
422 :unprocessable_entity
423 :locked
424 :failed_dependency
426 :upgrade_required
428 :precondition_required
429 :too_many_requests
431 :request_header_fields_too_large
500 :internal_server_error
501 :not_implemented
502 :bad_gateway
503 :service_unavailable
504 :gateway_timeout
505 :http_version_not_supported
506 :variant_also_negotiates
507 :insufficient_storage
508 :loop_detected
510 :not_extended
511 :network_authentication_required
在 Layouts and Rendering in Rails 可以找到。
測試 Redirect
expect(response).to redirect_to(home_path)
測試 Flash
expect(flash[:alert]).to eq "content you expected"
綜合範例:
class SubscriptionsController < ApplicationController
before_action :authenticate_user
def create
@subscription = Subscription.new(model_params)
if @subscription.save
redirect_to subscription_path(@subscription), notice: I18n.t("flash.subscription_succeeded")
else
redirect_to root_path, notice: I18n.t("flash.subscription_failed")
end
end
end
RSpec.describe SubscriptionsController do
describe "POST #create" do
context "success" do
it "redirects and flash" do
user = create(:user)
sign_in(user)
do_request
expect(user.subscription).to be_present
expect(response).to redirect_to subscription_path
expect(flash[:alert]).to eq I18n.t("flash.subscription_succeeded")
end
end
context "failed" do
it "redirects and flash" do
sign_in create(:user, :with_subscription)
do_request
expect(response).to redirect_to root_path
expect(flash[:alert]).to eq I18n.t("flash.subscription_failed")
end
end
end
end
Shoulda Matchers 提供跟 Controller 相關的方法
https://github.com/thoughtbot/shoulda-matchers#actioncontroller-matchers
延伸閱讀
Rails 關於測試 Controller 的文章:Functional Tests for Your Controllers。