Ruby
Rails
RSpec
controller

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

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

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

以上です。

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