Ruby on Railsを使って以前作ったサイトの再構築をしてます。
色々調べながらRSpecでコントローラのテストを書いてみたので手順をメモ。
Rails、RSpecともに初心者なので間違ってたらどんどん指摘ください。
環境
- ruby 2.2.2p95
- Rails 4.2.2
- RSpec 3.1.7
テスト対象
Ruby on Rails チュートリアル:実例を使って Rails を学ぼう
よくありがちなUserController
を対象にしてみます。(ほぼRailsチュートリアルで作ったもの)
Railsで、URLにIDでなく名前を入力して、アクセスする方法 - Qiita
作ってるサイトでは上記記事を参考に、/users/naichilab
みたいなURLでアクセスできるようにしています。
UserController
に関係するrake routes
はこんな感じ
users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:permalink/edit(.:format) users#edit user GET /users/:permalink(.:format) users#show PATCH /users/:permalink(.:format) users#update PUT /users/:permalink(.:format) users#update DELETE /users/:permalink(.:format) users#destroy
RSpecのインストール
参考書籍:Read Everyday Rails - RSpecによるRailsテスト入門 | Leanpub
自分はこの本を参考にインストールした。とてもわかりやすい。
FactoryGirlの設定方法とかも全部この本の通りにやった。
コントローラのテストって何するの?
参考サイト:Rubyist Magazine - スはスペックのス 【第 2 回】 RSpec on Rails (コントローラとビュー編)
古い記事だけど上記の記事を読んでかなり理解が進みました。
- RSpecで行えるのは単体テスト
- 単体テストでは責務をテストする
- コントローラの責務とは??
コントローラの責務
- 受信したリクエストに対して適切なレスポンスを返す
- リクエストに対してHTTPレスポンスがステータスコード200を返す。とか。
- ビューで使用するのに必要なモデルオブジェクトをロードする
- リクエストされたURLから必要なモデルインスタンスをロードしておく。とか。
- レスポンスを表示するのに適切なビューを選択する
- 適切なテンプレートを表示している。とか。
ふむふむ。それをかけばいいのね。
テスト書いてみる
どこに書くの?
/spec/controllers/users_controller_spec.rb
に書けばいいみたい。
アウトラインを書く
参考書籍にある通り、最初はUsersController
のbefore_action
を無効化して書き始めることにする。
class UsersController < ApplicationController # before_action :logged_in_user, only: [:edit, :update, :destroy] # before_action :correct_user, only: [:edit, :update] # before_action :admin_user, only: :destroy
ややこしくなりそうだから一歩ずつね。
rake routes
で出てくる7つのルーティングに対して先ほどの3つの責務は何か?を考えながら書いてみた。
require 'rails_helper' describe UsersController do describe 'Get #new' do it 'リクエストは200 OKとなること' it '@userに新しいユーザーを割り当てること' it ':newテンプレートを表示すること' end describe 'Get #index' do it 'リクエストは200 OKとなること' it '@usersに全てのユーザーを割り当てること' it ':indexテンプレートを表示すること' end describe 'Get #edit' do it 'リクエストは200 OKとなること' it '@userに要求されたユーザーを割り当てること' it ':editテンプレートを表示すること' end describe 'Get #show' do context '要求されたユーザーが存在する場合' do it 'リクエストは200 OKとなること' it '@userに要求されたユーザーを割り当てること' it ':showテンプレートを表示すること' end context '要求されたユーザーが存在しない場合' do it 'リクエストはRecordNotFoundとなること' end end describe 'Post #create' do context '有効なパラメータの場合' do it 'リクエストは302 リダイレクトとなること' it 'データベースに新しいユーザーが登録されること' it 'rootにリダイレクトすること' end context '無効なパラメータの場合' do it 'リクエストは200 OKとなること' it 'データベースに新しいユーザーが登録されないこと' it ':newテンプレートを再表示すること' end end describe 'Patch #update' do context '存在するユーザーの場合' do context '有効なパラメータの場合' do it 'リクエストは302 リダイレクトとなること' it 'データベースのユーザーが更新されること' it 'users#showにリダイレクトすること' end context '無効なパラメータの場合' do it 'リクエストは200 OKとなること' it 'データベースのユーザーは更新されないこと' it ':editテンプレートを再表示すること' end end context '要求されたユーザーが存在しない場合' do it 'リクエストはRecordNotFoundとなること' end end describe 'Delete #destroy' do context '存在するユーザーの場合' do it 'リクエストは302 リダイレクトとなること' it 'データベースから要求されたユーザーが削除されること' it 'users#indexにリダイレクトされること' end context '要求されたユーザーが存在しない場合' do it 'リクエストはRecordNotFoundとなること' end end end
合ってるかは分からん。
英語で書こうとするとそれだけでちょっとしんどいので思い切って日本語で書くことにした。
describe
とcontext
の使い分けは
describe
は機能に関するアウトラインを記述context
は特定の状態に関するアウトラインを記述
って感じらしい。全部describe
でも動くと思う。
ここまででbundle exec rspec spec/controllers/users_controller_spec.rb
を実行するとこんな感じ。
まだ中身書いてないからすべて黄色(pending)。
すでに読みやすい。
中身を実装していく
いったん書き上げたものを載せますけど以下の点は注意。
- 冒頭にも描いたけど
before_action
外してあるのでこのままではダメ - 明らかにコードの重複が多いしこのままじゃダメ
require 'rails_helper' describe UsersController do describe 'Get #new' do before do get :new end it 'リクエストは200 OKとなること' do expect(response.status).to eq 200 end it '@userに新しいユーザーを割り当てること' do expect(assigns(:user)).to be_a_new(User) end it ':newテンプレートを表示すること' do expect(response).to render_template :new end end describe 'Get #index' do before do @alice = create(:user, name: "alice") @bob = create(:user, name: "bob") get 'index' end it 'リクエストは200 OKとなること' do expect(response.status).to eq 200 end it '@usersに全てのユーザーを割り当てること' do expect(assigns(:users)).to match_array([@alice,@bob]) end it ':indexテンプレートを表示すること' do expect(response).to render_template :index end end describe 'Get #edit' do before do @user = create(:user) get 'edit', permalink: @user.permalink end it 'リクエストは200 OKとなること' do expect(response.status).to eq 200 end it '@userに要求されたユーザーを割り当てること' do expect(assigns(:user)).to eq @user end it ':editテンプレートを表示すること' do expect(response).to render_template :edit end end describe 'Get #show' do before do @user = create(:user) end context '要求されたユーザーが存在する場合' do before do get 'show', permalink: @user.permalink end it 'リクエストは200 OKとなること' do expect(response.status).to eq 200 end it '@userに要求されたユーザーを割り当てること' do expect(assigns(:user)).to eq @user end it ':showテンプレートを表示すること' do expect(response).to render_template :show end end context '要求されたユーザーが存在しない場合' do it 'リクエストはRecordNotFoundとなること' do expect{ get 'show', permalink: 'hogehoge' }.to raise_exception(ActiveRecord::RecordNotFound) end end end describe 'Post #create' do context '有効なパラメータの場合' do before do @user = attributes_for(:user) end it 'リクエストは302 リダイレクトとなること' do post :create, user: @user expect(response.status).to eq 302 end it 'データベースに新しいユーザーが登録されること' do expect{ post :create, user: @user }.to change(User, :count).by(1) end it 'rootにリダイレクトすること' do post :create, user: @user expect(response).to redirect_to root_path end end context '無効なパラメータの場合' do before do @invalid_user = attributes_for(:invalid_user) end it 'リクエストは200 OKとなること' do post :create, user: @invalid_user expect(response.status).to eq 200 end it 'データベースに新しいユーザーが登録されないこと' do expect{ post :create, user: @invalid_user }.not_to change(User, :count) end it ':newテンプレートを再表示すること' do post :create, user: @invalid_user expect(response).to render_template :new end end end describe 'Patch #update' do context '存在するユーザーの場合' do before do @user = create(:user) @originalname = @user.name end context '有効なパラメータの場合' do before do patch :update, permalink: @user.permalink, user: attributes_for(:user, name: 'hogehoge') end it 'リクエストは302 リダイレクトとなること' do expect(response.status).to eq 302 end it 'データベースのユーザーが更新されること' do @user.reload expect(@user.name).to eq 'hogehoge' end it 'users#showにリダイレクトすること' do expect(response).to redirect_to user_path(assigns(:user).permalink) end end context '無効なパラメータの場合' do before do patch :update, permalink: @user.permalink, user: attributes_for(:user, name: ' ') end it 'リクエストは200 OKとなること' do expect(response.status).to eq 200 end it 'データベースのユーザーは更新されないこと' do @user.reload expect(@user.name).to eq @originalname end it ':editテンプレートを再表示すること' do expect(response).to render_template :edit end end end context '要求されたユーザーが存在しない場合' do it 'リクエストはRecordNotFoundとなること' do expect{ patch :update, permalink: 'hogehoge' , user: attributes_for(:user) }.to raise_exception(ActiveRecord::RecordNotFound) end end end describe 'Delete #destroy' do before do @user = create(:user) end context '存在するユーザーの場合' do it 'リクエストは302 リダイレクトとなること' do delete :destroy, permalink: @user.permalink expect(response.status).to eq 302 end it 'データベースから要求されたユーザーが削除されること' do expect{ delete :destroy, permalink: @user.permalink }.to change(User,:count).by(-1) end it 'users#indexにリダイレクトされること'do delete :destroy, permalink: @user.permalink expect(response).to redirect_to users_path end end context '要求されたユーザーが存在しない場合' do it 'リクエストはRecordNotFoundとなること' do expect{ delete :destroy, permalink: 'hogehoge' }.to raise_exception(ActiveRecord::RecordNotFound) end end end end
GET
は書きやすかった。before
内でページにアクセスしておいて、it
で検証していく感じ。
POST
は登録できたことを確認するためにexpect{ post ~ }.to change().by(1)
ってやるみたいだけど、そのために各it
内でpostする形になってしまった…。
さすがにこの書き方はないと思うけどやり方がわからない。あとで調べる。
PATCH
はGET
に似た感じで書けた。
DELETE
とGET
は存在しないユーザーのidにhogehoge
って適当な文字列与えたけどどうやるのがいいのかな?
実行結果
全部緑になった。それっぽい。
とりあえず動いたけどコレジャナイ感がすごいので 本見ながらリファクタリングして続きの記事書きます。
お仕事でRuby on Rails
使ってる皆さん、ツッコミお待ちしています…!