LoginSignup
32
25

More than 5 years have passed since last update.

RSpecによるコントローラのテストで苦労したところ

Last updated at Posted at 2017-07-08

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

ファイル構成

用意しているファイルは以下の通りです。
スクリーンショット 2017-07-08 12.01.17.png

 では、テストコードを書いていきましょう!

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

※改善点などあればぜひご指導ください。

以上です。

果たしてこんな初心者が書いてよかったのか分かりませんが、自分自身昨日の学習を振り返ることができ、とても勉強になりました。
お読みいただき、ありがとうございました。

32
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
25