hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

投稿機能テスト

モデルのテストのステップ

モデルのテストを完成させるためのステップは以下の通り。

  1. RSpecの導入
  2. その他テストで使うgemの導入
  3. RSpecでテストを行う

1. テストで使うgemの導入

まずは、テストで使用するGemをアプリケーションに導入する。

 問題1:テストで使うgemを導入する

ヒント:カリキュラムをさかのぼったり、自身で調べて導入まで行う
ヒント:ダミーデータの生成や、ファクトリを生成するなどテストを効率的に行うためのgemも併せて導入する

問題2:導入したgemをインストールする

ヒント:問題1でbundle installしたテスト用のgemを使用するためには、ターミナル上であるコマンドを実行する必要がある
Gemfile
1
2
3
4
5
6
7
8
group :development, :test do
  gem 'byebug', platform: :mri
  gem 'pry-rails'
  gem 'rspec-rails', '~> 3.5'
  gem 'rails-controller-testing'
  gem 'factory_bot_rails'
  gem 'faker'
end
ターミナル
1
2
3
4
# チャットスペースのディレクトリに移動
$ cd chat-space
# bundle install
$ bundle install

解説

まずはRSpecを利用するためのrspec-rails、そして簡単にダミーのインスタンスを作成することができるfactory_bot_rails、ダミーデータを作成するためのfaker、コントローラのテストに必要なrails-controller-testingの4つのGemをインストール。

テスト環境で使用するのでgroupはtestの場所に記入する。
その後、bundle installを実行。

 
 
 
ターミナル
1
$ rails g rspec:install
1
2
# 下記の記述を追加
--format documentation

解説

ターミナルで上記のコマンドを入力するとrspecがインストールされる。
下記のファイルが作成されれば成功。

ターミナル
1
2
3
4
  create  .rspec
  create  spec
  create  spec/spec_helper.rb
  create  spec/rails_helper.rb

最後に、作成された「.rspec」ファイルを開き、--format documentationの記述を行う。これで、テストの結果が見やすくなる。

 

RSpecでテストを行う

実際にテストを行っていく。
今回は、以下の項目をテストしていく。
モデルのテストが終わったら次のコントローラのテストに進む。

  • メッセージを保存できる場合
    • メッセージがあれば保存できる
    • 画像があれば保存できる
    • メッセージと画像があれば保存できる
  • メッセージを保存できない場合
    • メッセージも画像も無いと保存できない
    • group_idが無いと保存できない
    • user_idが無いと保存できない

 問題3:FactoryBotを使用する準備をする

ヒント:テストで使用するデータをFactoryBotを用いて定義
ヒント:FactoryBotはある設定を行うことで記述を省略することができる

(模範解答で使用した画像のファイル名:test_image.jpg)

spec/factories/groups.rb
1
2
3
4
5
FactoryBot.define do
  factory :group do
    name {Faker::Team.name}
  end
end
spec/factories/users.rb
1
2
3
4
5
6
7
8
9
FactoryBot.define do
  factory :user do
    password = Faker::Internet.password(min_length: 8)
    name {Faker::Name.last_name}
    email {Faker::Internet.free_email}
    password {password}
    password_confirmation {password}
  end
end
spec/factories/messages.rb
1
2
3
4
5
6
7
8
FactoryBot.define do
  factory :message do
    content {Faker::Lorem.sentence}
    image {File.open("#{Rails.root}/public/images/test_image.jpg")}
    user
    group
  end
end
/spec/rails_helper.rb
1
2
3
4
5
#〜省略〜
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
  #〜省略〜
end

解説

FactoryBotのデータの定義

FactoryBotfakerを組み合わせることによって、テストで使用するデータを簡単に定義できる。

FactoryBotの記法を省略するために、rails_helper.rbに以下の記述を加える。

/spec/rails_helper.rb
1
2
3
4
5
#〜省略〜
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
  #〜省略〜
end

これで、rspecでFactoryBotを使用する準備ができた。

また、画像データについてはFakerで生成しない。
テスト用の画像を用意し、/public/images/の中に配置する。

ダミー画像の準備

1. publicディレクトリの中にimagesディレクトリを作成する

まずはご自身のchat-spaceのpublicディレクトリにimagesというディレクトリを作成する。

2. 適当な画像の名前をtest_image.jpgに変更する

imagesディレクトリが作成できたら、次にMacに適当な画像を用意。
この画像をダミー画像として用いる。
適当な画像が用意できたら、それをtest_image.jpgに名前を変更する。

3. 名前変更したtest_image.jpgをpublic/imagesに配置する

用意したtest_image.jpgをpublic/imagesに配置できたら、ダミー画像の用意は完了。

このダミー画像はこの後のモデルのテストで用いるので、用意ができたらそのままで大丈夫。

 

手順の解説

今回の目的は、先ほど用意したダミー画像をrspecのテストで用いる画像として指定すること。

spec/factories/messages.rb
1
2
3
4
5
6
7
8
FactoryBot.define do
  factory :message do
    content {Faker::Lorem.sentence}
    image {File.open("#{Rails.root}/public/images/test_image.jpg")}
    user
    group
  end
end

上記のコードの以下の部分にパスを設定している。

1
image {File.open("#{Rails.root}/public/images/test_image.jpg")}

こちらは~/アプリケーション名/public/images.test_image.jpgの画像をテストで用いるという意味。

Rails.rootの意味は/Users/~~/アプリケーションまでのパスを取得している。
詳しくはrails cをすれば確認できる。

 

 

 画像は文字列をファクトリに記述するのではなく、画像を用意して、その画像を用いてファクトリに値を用意する必要がある。
rspec carrierwave」といった言葉で調べてみる。

 問題4:messageモデルのテストを行う

ヒント:保存できる場合、保存できない場合を条件にテストをグループ分けする
/spec/models/message_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
require 'rails_helper'

RSpec.describe Message, type: :model do
  describe '#create' do
    context 'can save' do
      it 'is valid with content' do
        expect(build(:message, image: nil)).to be_valid
      end

      it 'is valid with image' do
        expect(build(:message, content: nil)).to be_valid
      end

      it 'is valid with content and image' do
        expect(build(:message)).to be_valid
      end
    end

    context 'can not save' do
      it 'is invalid without content and image' do
        message = build(:message, content: nil, image: nil)
        message.valid?
        expect(message.errors[:content]).to include("を入力してください")
      end

      it 'is invalid without group_id' do
        message = build(:message, group_id: nil)
        message.valid?
        expect(message.errors[:group]).to include("を入力してください")
      end

      it 'is invaid without user_id' do
        message = build(:message, user_id: nil)
        message.valid?
        expect(message.errors[:user]).to include("を入力してください")
      end
    end
  end
end

解説

モデルでテストすべきは以下の点。

  • メッセージを保存できる場合
    • メッセージがあれば保存できる
    • 画像があれば保存できる
    • メッセージと画像があれば保存できる
  • メッセージを保存できない場合
    • メッセージも画像も無いと保存できない
    • group_idが無いと保存できない
    • user_idが無いと保存できない

今回の場合、テストのケースがメッセージを保存できる場合メッセージを保存できない場合で分かれている。

このように、特定の条件でテストをグループ分けしたい場合、contextを使うことができる。

/spec/models/message_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
require 'rails_helper'

RSpec.describe Message, type: :model do
  describe '#create' do
    context 'can save' do
 # この中にメッセージを保存できる場合のテストを記述
    end

    context 'can not save' do
 # この中にメッセージを保存できない場合のテストを記述
    end
  end
end

contextを使用することによって、テストが条件毎にまとまって読みやすくなるので、積極的に活用するとよい。

 

メッセージを保存できる場合のテスト

メッセージがあれば保存できる

既にファクトリーの定義が完了しているため、build(:message)でMessageモデルのインスタンスを生成することができる。

/spec/models/message_spec.rb
1
2
3
 it 'is valid with content' do
  expect(build(:message)).to be_valid
 end

buildメソッドは、カラム名: 値の形で引数を渡すことによって、ファクトリーで定義されたデフォルトの値を上書きすることができる。今回は、メッセージがあれば保存できることを確かめたいので、image: nilを引数として、画像を持っていないインスタンスを生成する。

/spec/models/message_spec.rb
1
2
3
 it 'is valid with content' do
  expect(build(:message, image:  nil)).to be_valid
 end

画像があれば保存できる

/spec/models/message_spec.rb
1
2
3
 it 'is valid with image' do
  expect(build(:message, content:  nil)).to be_valid
 end

同様に、画像があれば保存できる場合についても、content: nilをbuildメソッドの引数とすることによって、メッセージを持っていないインスタンスを生成することができる。

メッセージと画像があれば保存できる

/spec/models/message_spec.rb
1
2
3
 it 'is valid with content and image' do
  expect(build(:message)).to be_valid
 end

メッセージと画像があれば保存できる場合は、ファクトリーでデフォルトの値が定義されているので、build(:message)と記述するだけで、メッセージと画像を持ったインスタンスを生成できる。

メッセージを保存できない場合のテスト

メッセージも画像も無いと保存できない

メッセージも画像もないと保存できない場合については、buildメソッドの引数でメッセージも画像もnilにすることによって、必要なインスタンスを生成することができる。

/spec/models/message_spec.rb
1
2
3
 it 'is invalid without content and image' do
   message = build(:message, content: nil, image: nil)
 end

作成したインスタンスがバリデーションによって保存ができない状態かチェックするため、valid?メソッドを利用する。

/spec/models/message_spec.rb
1
2
3
4
 it 'is invalid without content and image' do
   message = build(:message, content: nil, image: nil)
   message.valid?
 end

valid?メソッドを利用したインスタンスに対して、errorsメソッドを使用することによって、バリデーションにより保存ができない状態である場合なぜできないのかを確認することができる。

contentもimageもnilの今回の場合、'を入力してください'というエラーメッセージが含まれることが分かっているため、includeマッチャを用いて以下のようにテストを記述することができる。

 今回はエラー文を日本語化しているため、エラーメッセージも日本語のものでテストする。他国語や文言が違う場合は、実際にエラーメッセージとして扱われる文言に変更する必要がある。

/spec/models/message_spec.rb
1
2
3
4
5
it 'is invalid without content and image' do
  message = build(:message, content: nil, image: nil)
  message.valid?
  expect(message.errors[:content]).to include("を入力してください")
end

expectの引数に関して、message.errors[:カラム名]と記述することによって、そのカラムが原因のエラー文が入った配列を取り出すことができる。こちらに対して、includeマッチャを利用してエクスペクテーションを作っている。

user_idが無いと保存できない かつ group_idが無いと保存できない

最後に、group_idが無いと保存できない場合、user_idが無いと保存できない場合に関してだが、これらもメッセージも画像もないと保存できない場合と同じ方法でテストを書くことができる。

/spec/models/message_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 it 'is invalid without group_id' do
   message = build(:message, group_id: nil)
   message.valid?
   expect(message.errors[:group]).to include("を入力してください")
 end

 it 'is invaid without user_id' do
   message = build(:message, user_id: nil)
   message.valid?
   expect(message.errors[:user]).to include("を入力してください")
 end

テストの実行結果

各テストコードの記述が終わったら、テストを実行。

ターミナル
1
$ bundle exec rspec spec/models/message_spec.rb

以下のような結果になれば成功。

ターミナル
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ bundle exec rspec spec/models/message_spec.rb

Message
  #create
    can save
      is valid with content
      is valid with image
      is valid with content and image
    can not save
      is invalid without content and image
      is invalid without group_id
      is invaid without user_id

Finished in 0.53507 seconds (files took 4.17 seconds to load)
6 examples, 0 failures

 

コントローラのテストのステップ

コントローラのテストを完成させるためのステップは以下の通り。

  1. deviseをテストで使用するための設定
  2. RSpecでテストを行う

1. deviseをテストで使用するための設定

コントローラでは、ログインしている場合としていない場合で動作が異なる。
ログイン状態とログインしていない状態を分けてテストできるように設定する。
ログインをしている、していないということはgemのdeviseに任せているので、deviseのテストについて調べる必要がある。

 問題5:deviseをrspecで使用できるようにする

/spec/support/controller_macros.rb
1
2
3
4
5
6
module ControllerMacros
  def login(user)
    @request.env["devise.mapping"] = Devise.mappings[:user]
    sign_in user
  end
end
/spec/rails_helper.rb
1
2
3
4
5
6
RSpec.configure do |config|
  Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include ControllerMacros, type: :controller
  #〜省略〜
end

解説

まず、/spec/supportディレクトリに、controller_macros.rbを作成し、
loginメソッドを定義する。

/spec/support/controller_macros.rb
1
2
3
4
5
6
module ControllerMacros
  def login(user)
    @request.env["devise.mapping"] = Devise.mappings[:user]
    sign_in user
  end
end

その後、rails_helper.rbに、deviseのコントローラのテスト用のモジュールと、先ほど定義したControllerMacrosを読み込む記述を行う。

/spec/rails_helper.rb
1
2
3
4
5
6
RSpec.configure do |config|
  Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include ControllerMacros, type: :controller
  #〜省略〜
end

これで、deviseをrspecで使用する準備ができた。

模範回答の通りに記述を行なってもエラーになる場合、下記設定を確認する。

rails_helper内のDir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }の記述のコメントアウトを外す。

1
2
3
4
5
    # 修正前
    # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

    # 修正後
    Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

 

2. RSpecでテストを行う


今回は、以下の項目をテストしていく。

メッセージ一覧ページを表示するアクション

  • ログインしている場合
    • アクション内で定義しているインスタンス変数があるか
    • 該当するビューが描画されているか
  • ログインしていない場合
    • 意図したビューにリダイレクトできているか

 問題6:messagesコントローラでメッセージ一覧を表示するアクションのテストを行う

ヒント:モデルのテストと同様に、今回も条件でテストを場合分けしてみる
spec/controllers/messages_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
require 'rails_helper'

describe MessagesController do
  let(:group) { create(:group) }
  let(:user) { create(:user) }

  describe '#index' do

    context 'log in' do
      before do
        login user
        get :index, params: { group_id: group.id }
      end

      it 'assigns @message' do
        expect(assigns(:message)).to be_a_new(Message)
      end

      it 'assigns @group' do
        expect(assigns(:group)).to eq group
      end

      it 'renders index' do
        expect(response).to render_template :index
      end
    end

    context 'not log in' do
      before do
        get :index, params: { group_id: group.id }
      end

      it 'redirects to new_user_session_path' do
        expect(response).to redirect_to(new_user_session_path)
      end
    end
  end
end

解説

メッセージ一覧を表示するアクションでテストすべきは以下の点。

  • ログインしている場合
    • アクション内で定義しているインスタンス変数があるか
    • 該当するビューが描画されているか
  • ログインしていない場合
    • 意図したビューにリダイレクトできているか

今回はログインをしているかどうかを条件に、contextを用いてテストをグループ分けしてみる。

messages_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
require 'rails_helper'

describe MessagesController do

  describe '#index' do
    context 'log in' do
# この中にログインしている場合のテストを記述
    end

    context 'not log in' do
# この中にログインしていない場合のテストを記述
    end
  end

続いて、今回のテストで必要なインスタンスを生成する記述を行う。複数のexampleで同一のインスタンスを使いたい場合、letメソッドを利用することができる。

messages_controller_spec.rb
1
2
3
4
5
6
7
8
9
require 'rails_helper'

describe MessagesController do
#  letを利用してテスト中使用するインスタンスを定義
  let(:group) { create(:group) }
  let(:user) { create(:user) }

  describe '#index' do
# 〜省略

letメソッドは呼び出された際に初めて実行される、遅延評価という特徴を持っている。後述のbeforeメソッドが各exampleの実行前に毎回処理を行うのに対し、letメソッドは初回の呼び出し時のみ実行される。複数回行われる処理を一度の処理で実装できるため、テストを高速にすることができる。また、一度実行された後は常に同じ値が返って来るため、テストで使用したいオブジェクトの定義に適している。

テストに必要なインスタンスが用意できたので、今度は「ログインをしている場合」「ログインをしていない場合」を実際に再現していく。

messages_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
require 'rails_helper'

describe MessagesController do

  describe '#index' do
    context 'log in' do
      before do
        login user
        get :index, params: { group_id: group.id }
      end
# この中にログインしている場合のテストを記述
    end

    context 'not log in' do
      before do
        get :index, params: { group_id: group.id }
      end
# この中にログインしていない場合のテストを記述
    end
  end

beforeブロックの内部に記述された処理は、各exampleが実行される直前に、毎回実行される。beforeブロックに共通の処理をまとめることで、コードの量が減り、読みやすいテストを書くことができる。

「ログインをする」、「擬似的にindexアクションを動かすリクエストを行う」が共通の処理となるため、beforeの内部に記述している。

deviseを用いて「ログインをする」ためのloginメソッドは、問題5で定義したもの。

/spec/support/controller_macros.rb
1
2
3
4
5
6
module ControllerMacros
  def login(user)
    @request.env["devise.mapping"] = Devise.mappings[:user]
    sign_in user
  end
end

「擬似的にindexアクションを動かすリクエストを行う」ために、getメソッドを利用。messagesのルーティングはgroupsにネストされているため、group_idを含んだパスを生成する。そのため、getメソッドの引数として、params: { group_id: group.id }を渡している。

ここまでで、各テスト共通の処理を定義することができた。

 

テスト本体の記述を行う。

  • ログインしている場合

まず、アクション内で定義しているインスタンス変数があるかどうか確かめる。
インスタンス変数に代入されたオブジェクトは、コントローラのassigns メソッド経由で参照できる。
@messageを参照したい場合、assigns(:message)と記述することができる。

messages_controller_spec.rb
1
2
3
4
5
6
7
      it 'assigns @message' do
        expect(assigns(:message)).to be_a_new(Message)
      end

      it 'assigns @group' do
        expect(assigns(:group)).to eq group
      end

@messageはMessage.newで定義された新しいMessageクラスのインスタンスbe_a_newマッチャを利用することで、 対象が引数で指定したクラスのインスタンスかつ未保存のレコードであるかどうか確かめることができる。今回の場合は、assigns(:message)がMessageクラスのインスタンスかつ未保存かどうかをチェックしている。

messages_controller_spec.rb
1
2
3
      it 'assigns @group' do
        expect(assigns(:group)).to eq group
      end

@groupはeqマッチャを利用してassigns(:group)groupが同一であることを確かめることでテストできる。

続いて、該当するビューが描画されているかどうかをテストする。

messages_controller_spec.rb
1
2
3
      it 'renders index' do
        expect(response).to render_template :index
      end

expectの引数にresponseを渡している。responseは、example内でリクエストが行われた後の遷移先のビューの情報を持つインスタンス。 render_templateマッチャは引数にアクション名を取り、引数で指定されたアクションがリクエストされた時に自動的に遷移するビューを返す。この二つを合わせることによって、example内でリクエストが行われた時の遷移先のビューが、indexアクションのビューと同じかどうか確かめることができる。

  • ログインしていない場合

最後に、ログインしていない場合に、意図したビューにリダイレクトできているかをテストする。

messages_controller_spec.rb
1
2
3
it 'redirects to new_user_session_path' do
  expect(response).to redirect_to(new_user_session_path)
end

redirect_toマッチャは引数にとったプレフィックスにリダイレクトした際の情報を返すマッチャ。今回の場合は、非ログイン時にmessagesコントローラのindexアクションを動かすリクエストが行われた際に、ログイン画面にリダイレクトするかどうかを確かめる記述になっている。

各テストコードの記述が終わったら、テストを実行。

ターミナル
1
$ bundle exec rspec spec/controllers/messages_controller_spec.rb

以下のような結果になれば成功。

ターミナル
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ bundle exec rspec spec/controllers/messages_controller_spec.rb

MessagesController
  #index
    log in
      assigns @message
      assigns @group
      renders index
    not log in
      redirects to new_user_session_path

Finished in 0.22727 seconds (files took 3.8 seconds to load)
4 examples, 0 failures

 

メッセージを作成するアクション

  • ログインしているかつ、保存に成功した場合
    • メッセージの保存はできたのか
    • 意図した画面に遷移しているか
  • ログインしているが、保存に失敗した場合
    • メッセージの保存は行われなかったか
    • 意図したビューが描画されているか
  • ログインしていない場合
    • 意図した画面にリダイレクトできているか

 問題7:messagesコントローラでメッセージを作成するアクションのテストを行う

spec/controllers/messages_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#〜省略〜
  describe '#create' do
    let(:params) { { group_id: group.id, user_id: user.id, message: attributes_for(:message) } }

    context 'log in' do
      before do
        login user
      end

      context 'can save' do
        subject {
          post :create,
          params: params
        }

        it 'count up message' do
          expect{ subject }.to change(Message, :count).by(1)
        end

        it 'redirects to group_messages_path' do
          subject
          expect(response).to redirect_to(group_messages_path(group))
        end
      end

      context 'can not save' do
        let(:invalid_params) { { group_id: group.id, user_id: user.id, message: attributes_for(:message, content: nil, image: nil) } }

        subject {
          post :create,
          params: invalid_params
        }

        it 'does not count up' do
          expect{ subject }.not_to change(Message, :count)
        end

        it 'renders index' do
          subject
          expect(response).to render_template :index
        end
      end
    end

    context 'not log in' do

      it 'redirects to new_user_session_path' do
        post :create, params: params
        expect(response).to redirect_to(new_user_session_path)
      end
    end
  end
end

メッセージを作成するアクションでテストすべきは以下の点。

  • ログインしているかつ、保存に成功した場合
    • メッセージの保存はできたのか
    • 意図した画面に遷移しているか
  • ログインしているが、保存に失敗した場合
    • メッセージの保存は行われなかったか
    • 意図したビューが描画されているか
  • ログインしていない場合
    • 意図した画面にリダイレクトできているか

下記の条件に基づいて、contextでグループ分けする。

  • ログインをしているかどうか
  • メッセージの保存に成功したかどうか
messages_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#〜省略〜
  describe '#create' do
    let(:params) { { group_id: group.id, user_id: user.id, message: attributes_for(:message) } }

    context 'log in' do
    # この中にログインしている場合のテストを記述
      before do
        login user
      end

      context 'can save' do
      # この中にメッセージの保存に成功した場合のテストを記述
      end

      context 'can not save' do
      # この中にメッセージの保存に失敗した場合のテストを記述
      end
    end

    context 'not log in' do
    # この中にログインしていない場合のテストを記述
      before do
        get :create, params: params
      end

    end
  end
end

contextは、contextブロックの内部でネストさせることができる。ネストさせることによって、ログインしている場合という条件の中で、さらに保存に成功した場合・失敗した場合を条件にグループを分けることができた。

3行目でletメソッドを用いてparamsを定義している。これは、擬似的にcreateアクションをリクエストする際に、引数として渡すためのもの。 attributes_forcreatebuild同様FactoryBotによって定義されるメソッドで、オブジェクトを生成せずにハッシュを生成するという特徴がある。

ターミナル
1
2
3
4
5
6
7
8
$ rails  c
[1] pry(main)> FactoryBot.build(:message)
=> #<Message:0x007fe0f0c42bf0 id: nil, content: "Quod suscipit aspernatur id delectus qui exercitationem.", image: nil, group_id: nil, user_id: nil, created_at: nil, updated_at: nil>
# オブジェクトを生成する

[2] pry(main)> FactoryBot.attributes_for(:message)
=> {:content=>"Quod suscipit aspernatur id delectus qui exercitationem.", :image=>#<File:/Users/yujimatsumoto/projects/a-project/public/images/no_image.jpg>}
# ハッシュを生成する

 

  • ログインしているかつ、保存に成功した場合

まず、メッセージの保存ができているのかどうかを確かめる。

messages_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#〜省略〜
context 'can save' do
  subject {
     post :create,
     params: params
   }

  it 'count up message' do
     expect{ subject }.to change(Message, :count).by(1)
  end

expectの引数として、subjectを定義して渡している。expectの引数が長くなってしまう際は、このようにして記述を切り出すことができる。このエクスペクテーションは、「postメソッドでcreateアクションを擬似的にリクエストをした結果」という意味になる。

createアクションのテストを行う際にはchangeマッチャを利用することができる。changeマッチャは引数が変化したかどうかを確かめるために利用できるマッチャ。change(Message, :count).by(1)と記述することによって、Messageモデルのレコードの総数が1個増えたかどうかを確かめることができる。保存に成功した際にはレコード数が必ず1個増えるため、このようなテストとなる。

続いて、意図した画面に遷移しているかどうかを確かめる。

messages_controller_spec.rb
1
2
3
4
5
#〜省略〜
it 'redirects to group_messages_path' do
  subject
  expect(response).to redirect_to(group_messages_path(group))
end

createアクションを動かした際のリダイレクト先はgroup_messages_pathなので、上記の形でテストを行うことができる。

  • ログインしているかつ、保存に失敗した場合 まず、メッセージの保存は行われなかったかどうかをテストしていく。
messages_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
context 'can not save' do
  let(:invalid_params) { { group_id: group.id, user_id: user.id, message: attributes_for(:message, content: nil, image: nil) } }

   subject {
     post :create,
     params: invalid_params
   }

  it 'does not count up' do
     expect{ subject }.not_to change(Message, :count)
  end
end

invalid_paramsを定義する際に、attributes_for(:message)の引数に、content: nilimage: nilと記述している。
擬似的にcreateアクションをリクエストする際にinvalid_paramsを引数として渡してあげることによって、意図的にメッセージの保存に失敗する場合を再現することができる。

Rspec「〜であること」を期待する場合にはtoを使用するが、「〜でないこと」を期待する場合にはnot_toを使用できる。not_to change(Message, :count)と記述することによって、「Messageモデルのレコード数が変化しないこと ≒ 保存に失敗したこと」を確かめることができる。

 

続いて、意図したビューが描画されているかどうかを確かめる。

messages_controller_spec.rb
1
2
3
4
it 'renders index' do
  subject
  expect(response).to render_template :index
end

メッセージの保存に失敗した場合、indexアクションのビューをrenderするよう設定されているので、上記の形でテストを行うことができる。

 

  • ログインしていない場合

最後に、非ログイン時に意図した画面にリダイレクトできているかどうかを確かめる。

messages_controller_spec.rb
1
2
3
4
5
6
7
context 'not log in' do

  it 'redirects to new_user_session_path' do
    post :create, params: params
    expect(response).to redirect_to(new_user_session_path)
  end
end

ログインしていない場合にcreateアクションをリクエストした際は、ログイン画面へとリダイレクトする。
redirect_toマッチャの引数に、new_user_session_pathを取ることで、ログイン画面へとリダイレクトしているかどうかを確かめることができる。

各テストコードの記述が終わったら、テストを実行する。

ターミナル
1
$ bundle exec rspec spec/controllers/messages_controller_spec.rb

下記のような結果になれば成功。

ターミナル
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ bundle exec rspec spec/controllers/messages_controller_spec.rb

MessagesController
  #index
    log in
      assigns @message
      assigns @group
      redners index
    not log in
      redirects to new_user_session_path
  #create
    log in
      can save
        count up message
        redirects to group_messages_path
      can not save
        does not count up
        renders index
    not log in
      redirects to new_user_session_path

Finished in 0.32079 seconds (files took 3.97 seconds to load)
9 examples, 0 failures

 

テストのポイント

テストを記述していく上でのポイントは、以下のようになる。

  • example1つにつき、結果を一つだけ期待している
  • どのテストも明示的である
  • 期待できる値が返ってくるときと、そうでないときを両方テストする
  • FactoryBotを上手く利用して、すっきりとまとめる
  • Fakerでダミーデータを作成する
  • テスト内で使用する変数は、インスタンス変数として定義するのではなく、letを使用する

rspec参考

faker参考リンク