RSpecによるコントローラのテスト
この記事を書いているのはプログラミング歴:1ヶ月の初心者です。
よろしくお願いします。
某学校のチャットアプリの課題で、二日間悩みに悩み、
非常に苦労したので、次のカリキュラムに進む前にまとめてみます。
とりあえず以下の方法でOKをもらいましたが、
なにぶん初心者ゆえに無駄な記述が多く、間違いもあると思います。
改善できる部分があれば教えていただけると助かります。
GEM
gem 'devise'
gem 'rspec-rails'
gem 'factory_girl_rails'
gem 'faker'
gem 'rails-controller-testing'
擬似的なログイン環境を作り出す。
まずログイン環境を作りましょう。
以下の記事が非常に参考になりました。この通りにやればできます。
rspecのテスト環境でdeviseにログインする方法【rails】
注意点
#messages_controller_spec.rb
describe MessaesController, type: :controller do
自分の場合はtype: :controllerを入力しておらず、実行すると以下のエラーが出てしまっていました。
#terminal
$ rspec messages_controller_spec.rb
=> undefined method `login_user'
モデルのテストとして認識してしまっていて、モデルにはログインという概念がない為でしょうか?
詳しいことは分かりませんでしたが、type: :controllerを入れることで解決できました。
ググると同じようなエラーが出ている人がいましたが、特に参考になる記事は見つかりませんでした
モデルのアソシエーション
今回テストするコントローラですが、モデルのアソシエーションは以下の関係になります。
# message.rb
belongs_to :user
belongs_to :group
# group.rb
has_many :members
has_many :users, through: :members
has_many :messages
# user.rb
has_many :members
has_many :groups, through: :members
has_many :messages
#member.rb(group_userの中間テーブル)
belongs_to :user
belongs_to :group
ルーティングの設定
今回はgroupsコントローラにmessagesコントローラをネストさせていますので、
groups/:group_id/messagesというパスになっています。
Rails.application.routes.draw do
devise_for :users
root 'groups#index'
resources :users, only: [:edit, :update]
resources :groups, only: [:index, :new, :create, :edit, :update] do
resources :messages, only: [:index, :create]
end
end
テストするコントローラ
# messages_controller.rb
class MessagesController < ApplicationController
before_action :current_user_groups, only: [:index, :create]
def index
@message = Message.new
end
def create
@message = Message.new(create_params)
if @message.save
redirect_to group_messages_path(params[:group_id])
else
flash.now[:alert] = "テキストが入力されていません"
render :index
end
end
private
def create_params
params.require(:message).permit(:text, :image).merge(group_id: params[:group_id], user_id: current_user.id)
end
def current_user_groups
@group = Group.find(params[:group_id])
@groups = current_user.groups.includes([:messages,:users])
@messages = @group.messages.includes(:user)
end
end
factory_girlの設定
# factories/message.rb
FactoryGirl.define do
# factory :message do
text { Faker::StarWars.wookie_sentence }
image { Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/images/test.jpg')) }
group
user
created_at { Faker::Time.between(5.days.ago, 3.days.ago, :all) }
updated_at { Faker::Time.between(2.days.ago, 1.days.ago, :all) }
end
end
# factories/group.rb
FactoryGirl.define do
factory :group do
name { Faker::Pokemon.name }
created_at { Faker::Time.between(5.days.ago, 3.days.ago, :all) }
updated_at { Faker::Time.between(2.days.ago, 1.days.ago, :all) }
end
end
# factories/member.rb
FactoryGirl.define do
factory :member do
user
group
end
end
# factories/member.rb
FactoryGirl.define do
pass = Faker::Internet.password(8)
factory :user do
name { Faker::Pokemon.name }
email { Faker::Internet.email }
password pass
password_confirmation pass
after(:create) do |user|
create(:member, user: user, group: create(:group))
end
end
end
ファイル構成
## では、テストコードを書いていきましょう!
messages#indexアクションのテスト
let文
factoryで用意したダミーをあらかじめletで定義しておきます。
#messages_controller_spec.rb
describe MessagesController, type: :controller do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:message) { build(:message) }
form_forで使う変数@message
※以下、describe、context、itの中は、できれば英語で書きましょうということでしたので、一応英語で書いています。つたない英語ですが、よろしくお願いします。
#messages_controller_spec.rb
it "is assigns sg @message" do
blank_message = Message.new
get :index, params: { group_id: group }
expect(assigns(:message).attributes).to eq(blank_message.attributes)
end
#messages_controller.rb
@message = Message.new
以上のように書きました。(教えてもらいました。)
最初attributesなしではテストがパスしませんでした。attributesを実行すると、元の変数がハッシュの形になって返ってきます。
変数@groupのテスト
#messages_controller_spec.rb
it "is assigns sg @group" do
get :index, params: { group_id: group }
expect(assigns(:group)).to eq(group)
end
#messages_controller.rb
@group = Group.find(params[:group_id])
特に何も言われませんでしたが、あんまり自身ないです。
変数@messagesのテスト
#messages_controller_spec.rb
it "is assigns pl @messages has current_group.users" do
messages = create_list(:message, 3, user_id: user.id, group_id: group.id)
get :index, params: { group_id: group }
expect(assigns(:messages)).to match(messages)
end
#messages_controller.rb
@messages = @group.messages.includes(:user)
グループに所属するメッセージが取得されているかテストしています。
create_listを実行する際に、factoryであらかじめ生成したuserとgroupにuser_idとgroup_idを指定して、複数のレコードを生成しました。
変数@groupsのテスト
#messages_controller_spec.rb
it "is assings pl @groups has current_user.groups" do
groups = create_list(:group, 3)
groups.each do |g|
g.members.create(user: user)
end
get :index, params: { group_id: groups.first.id }
groups = user.groups
expect(assigns(:groups)).to eq groups
end
#messages_controller.rb
@groups = current_user.groups.includes([:messages,:users])
テストしたいのは、現在ログインしているユーザーと関係のあるグループが送られてきているか、です。
どうしたら中間テーブルと関係のある複数のレコードを作れるのか、これが全く分かりませんでした。
each文で中間テーブルのレコードを作成することで、解決しています。
ビューに遷移しているか? は割愛しますが、ここまでの完成コードを載せておきます。
ここまでの完成コード
#messages_controller_spec.rb
describe 'GET #index' do
context 'when user login' do
before do
login_user user
end
it "is assigns sg @message" do
blank_message = Message.new
get :index, params: { group_id: group }
expect(assigns(:message).attributes).to eq(blank_message.attributes)
end
it "is assigns sg @group" do
get :index, params: { group_id: group }
expect(assigns(:group)).to eq(group)
end
it "is assigns pl @messages has current_group.users" do
messages = create_list(:message, 3, user_id: user.id, group_id: group.id)
get :index, params: { group_id: group }
expect(assigns(:messages)).to match(messages)
end
it "is assings pl @groups has current_user.groups" do
groups = create_list(:group, 3)
groups.each do |g|
g.members.create(user: user)
end
get :index, params: { group_id: groups.first.id }
groups = user.groups
expect(assigns(:groups)).to eq groups
end
it "renders index template?" do
get :index, params: { group_id: group }
expect(response).to render_template :index
end
end
※改善点などあればぜひご指導ください。
###messages#createアクションのテスト
バリデーション
# models/message.rb
validates :text, presence: true, unless:"image?"
@messageがDBに保存されているか?
#messages_controller_spec.rb
it 'is write in Database?' do
expect do
post :create, params: { group_id: group, message: attributes_for(:message) }
end.to change(Message, :count).by(1)
end
#messages_controller.rb
if @message.save
redirect_to group_messages_path(params[:group_id])
else
書き方がよくわからなかったので、この辺からもらってきました。モデルにカウントメソッドを使って、テーブル内のレコードの数を数えているという感じでしょうか。
Rails RSpecの基本 ~Controller編~
@messageがDBに保存されていないか?
#messages_controller_spec.rb
it 'is not write in Database?' do
expect do
post :create, params: { group_id: group, message: attributes_for(:message, text: nil, image: nil) }
end.to change(Message, :count).by(0)
end
#messages_controller.rb
else
flash.now[:alert] = "テキストが入力されていません"
render :index
end
意外と苦労したのが、保存できないパターンを作ることです。
#messages_controller_spec.rb
message = build(:user, text = nil, image = nil)
てな感じで書けば行けるんじゃないかと思ったんですが、どうやっても保存されるのはこの新しく定義したmessageではなくて、letで書いていた別のmessage。
あんまりうまく行かないんで質問したところ、paramsに直接attributes_forで指定するやり方で、解決しました。
完成コード
# messages_controller_spec.rb
require 'rails_helper'
describe MessagesController, type: :controller do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:message) { build(:message) }
describe 'GET #index' do
context 'when user login' do
before do
login_user user
end
it "is assigns sg @message" do
blank_message = Message.new
get :index, params: { group_id: group }
expect(assigns(:message).attributes).to eq(blank_message.attributes)
end
it "is assigns sg @group" do
get :index, params: { group_id: group }
expect(assigns(:group)).to eq(group)
end
it "is assigns pl @messages has current_group.users" do
messages = create_list(:message, 3, user_id: user.id, group_id: group.id)
get :index, params: { group_id: group }
expect(assigns(:messages)).to match(messages)
end
it "is assings pl @groups has current_user.groups" do
groups = create_list(:group, 3)
groups.each do |g|
g.members.create(user: user)
end
get :index, params: { group_id: groups.first.id }
groups = user.groups
expect(assigns(:groups)).to eq groups
end
it "renders index template?" do
get :index, params: { group_id: group }
expect(response).to render_template :index
end
end
context 'when user is not login' do
it "can redirect to new_user_session?" do
get :index, params: { group_id: group }
expect(response).to redirect_to new_user_session_path
end
end
end
describe 'post #create' do
context 'when user login and successed processing' do
before do
login_user user
end
it 'is write in Database?' do
expect do
post :create, params: { group_id: group, message: attributes_for(:message) }
end.to change(Message, :count).by(1)
end
it 'is redirect to index template?' do
post :create, params: { group_id: group, message: attributes_for(:message) }
expect(response).to redirect_to group_messages_path
end
end
context 'when user login and unsuccessed processing' do
before do
login_user user
end
it 'is not write in Database?' do
expect do
post :create, params: { group_id: group, message: attributes_for(:message, text: nil, image: nil) }
end.to change(Message, :count).by(0)
end
it "is redirect to index template?" do
message = build(:message, text:"")
post :create, params: { group_id: group, message: attributes_for(:message) }
expect(response).to redirect_to group_messages_path
end
end
context 'when user is not login' do
it "can redirect to new_user_session?" do
post :create, params: { group_id: group, message: attributes_for(:message) }
expect(response).to redirect_to new_user_session_path
end
it 'can not write in Database?' do
expect do
post :create, params: { group_id: group, message: attributes_for(:message) }
end.to change(Message, :count).by(0)
end
end
end
end
※改善点などあればぜひご指導ください。
#以上です。
果たしてこんな初心者が書いてよかったのか分かりませんが、自分自身昨日の学習を振り返ることができ、とても勉強になりました。
お読みいただき、ありがとうございました。