世界一わかりやすい!!複数画像投稿時のRSpecテストの書き方

目次

はじめに

クマくん
クマくん

困ったなぁ。

さめさん
さめさん

クマくん、どうしたの?

やけに深刻そうな顔して。

クマくん
クマくん

実は、accepts_nested_attirbutes_forを使った複数画像投稿の機能のテストをしているんだけど、全然うまくいかないんだ。

何がなんだかさっぱりわからないよぉ・・・

さめさん
さめさん

テストが通らないっていうのはある意味しょうがないと思うけど、何がわからないんだい?

もうちょっと具体的に教えてくれる?

クマくん
クマくん

僕が思っているような挙動にならないんだ。

具体的にはfactory_botで作ったテストデータがうまく作れないエラーなんだよね。

2つのテーブルの情報を同時に保存させたいんだけど、保存させるところでエラーが出ちゃって・・・

助けて〜〜〜

さめさん
さめさん

なるほどねぇ。

思い当たる節がいくつかあるね・・・

さめさん
さめさん

じゃあ今回は僕が複数画像を投稿する時のテストを一緒に作ろうか!

しっかりと勉強するんだよ!

クマくん
クマくん

サメさんありがとう〜〜〜!!

今回全くのお手上げだったからとても嬉しいなぁ〜!

前提

さめさん
さめさん

Ruby on the RailsにRSpecを導入していない人はrspec、factory_bot、fakerのgemを入れておいてね!

環境

Ruby on the Rails 5.2

Ruby 2.5.1

RSpec 4.0.0

carrierwave 2.1.0

複数画像投稿の実装のおさらい

さめさん
さめさん

じゃあ、早速どんな機能のテストをしたいか教えてくれるかな?

複数画像の投稿機能

クマちゃん
クマちゃん

accepts_nested_attributes_forで実際itemモデルでitemの情報を保存させるときに複数枚の画像を保存させようと思って。

画像の保存自体にはcarrierwaveのimage_uploaderを使っているんだけど。

さめさん
さめさん

ありがとう!

簡単にでいいからモデルとかの記述まとめてくれる?

クマちゃん
クマちゃん

OK〜!

モデル

クマちゃん
クマちゃん

Itemモデルだよ。

1対多の関係でimageモデルとアソシエーションを組んでいて、

accepts_nested_attirubtes_forメソッドで同時に保存できるようにしているんだ。

class Item < ApplicationRecord
  belongs_to :user
  has_many :images, dependent: :destroy
  accepts_nested_attributes_for :images, allow_destroy: true
  validates_associated :images

  with_options presence: true do
    validates :user_id
  end
end
クマちゃん
クマちゃん

Imageモデルだよ。

carrierwaveのimage_uploaderを使っているんだ。

class Image < ApplicationRecord
  belongs_to :item
  mount_uploader :photo, ImageUploader
end
クマちゃん
クマちゃん

userモデルだよ。

deviseを使ってユーザー登録をしているんだけど、細かな記述は省略するね。

class User < ApplicationRecord
  has_many :items
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  with_options presence: true do
    validates :nickname, uniqueness: true, length: { maximum: 8 }
    validates :email, uniqueness: true 
  end

end

コントローラー

クマちゃん
クマちゃん

Itemsコントローラーだよ。

色々設定はしてるんだけど、今回のテストのためだけに抜粋するね。

class ItemsController < ApplicationController
  def new
    @item = Item.new
    @images = @item.images.new
  end

  def create
    @item = Item.new(item_params)

    if @item.save
      redirect_to root_path
    else
      render :new
    end
  end

  private
  def item_params
    params.require(:item).permit(:name, images_attributes: [:photo]).merge(user_id: current_user.id)
  end

end

データベース設計

クマちゃん
クマちゃん

itemsテーブルだよ。

id
nameitemの名前
user_idユーザーの参照
created_at作成日
updated_at更新日
クマちゃん
クマちゃん

imagesテーブルだよ。

id
item_iditemsテーブルとの参照
photo写真のデータ保存
created_at作成日
updated_at更新日
クマちゃん
クマちゃん

usersテーブルだよ。

id
nicknameニックネーム
emailメールアドレス
encrypted_password暗号化されたパスワード
reset_password_token再設定用のトークン
reset_pasword_sent_atいつ再設定用の連絡をしたか
created_at作成日
updated_at更新日

RSpec

さめさん
さめさん

ありがとう。

大体どんなコードを書いているか分かったよ!

次に具体的なテストコードを書いていこう。

クマくん
クマくん

うん!ありがとう〜

テスト作成

さめさん
さめさん
 
 

まずはモデルの単体テストをやってみようか。

モデルの単体テストの作成手順
  • railsコマンドでspecファイルを作成しよう。
  • 何をテストしたいか決めよう。
  • テスト用のデータをfactory_botで作ろう。
テストを作るときの流れだよ

specファイルを作成する。

さめさん
さめさん

rails g rspec:model モデル名

ってコマンドでspecファイルを作成しよう。

クマくん
クマくん

今回の場合だとどうしたらいいかな。

itemモデルをテストをしたいんだけど。

さめさん
さめさん

そしたら

rails g rspec:model item

でいいかな。

モデルのテストを作るときはモデル名をそのまま書くんだ。

コマンドでファイルを作成した方が、打ち間違いもなく作れるから絶対にコマンドで作成してね!

 $ rails g rspec:model item

何をテストしたいのか決めよう

さめさん
さめさん

よし。

じゃあ早速何をテストしたいのか決めていこう。

クマくん
クマくん

今回だと、一番重要なのはimageモデルのデータとitemがそもそもエラーなく作れるか?かな。

そのほかにもitemモデルのnameカラムにnot null制約もかけているけど・・・

さめさん
さめさん

分かった。

それだったら、まずはitemをちゃんと作れるかだね。

itemはそのまま作れるのかな?

クマちゃん
クマちゃん

userがいないとitemは作れないようになっているよ。

さめさん
さめさん

よし、それじゃあitemを作る前にユーザーの情報を作成しよう。

それでitemが作れているか確認するためにマッチャはbe_validにしようか!

ポイント

・beforeを用いて各テストの前にユーザーを作成する。

・valid?メソッドを使って有効か確認する。

・be_valid?のマッチャを使ってテストする。

require 'rails_helper'

describe Item do
  describe '#create' do

    before do
      @user = FactoryBot.create(:user)
    end

    it "is valid with a name, images" do
      item = FactoryBot.build(:item)
      item.valid?
      expect(item).to be_valid
    end

  end
end

factoryBotの設定

さめさん
さめさん

よし。

じゃあ早速factory_botでテストデータを作っていこうか。

クマくんは何が必要だと思う?

クマくん
クマくん

うーん・・・

userとitemとimagesかな?

さめさん
さめさん

いいね!

今回はちょっと特別なやり方をするからuserとitemのfactory_botファイルを作ろう。

rails g factory_bot:model モデル名

のコマンドで作っていこう!

$ rails g factory_bot:model user
$ rails g factory_bot:model item
さめさん
さめさん

spec/factoriesの配下にファイルが作成されたかな?

クマくん
クマくん

できたよ〜!

さめさん
さめさん

spec/factories/user.rbをこのように記述しよう。

FactoryBot.define do

  factory :user do
    nickname {Faker::Name.name}
    email {Faker::Internet.free_email}
    password {password}
    password_confirmation {password}
  end

end
さめさん
さめさん

spec/factories/item.rbを記述しよう。

FactoryBot.define do
  factory :item do
    name {"美味しいサラダ"}
    user { FactoryBot.create(:user)}

    after(:build) do |item|
      item.images << FactoryBot.build(:image, item: item)
    end  
  end


  factory :image do
    photo   { Rack::Test::UploadedFile.new(File.join(Rails.root, "spec/fixtures/sample.png"), 'image/png') }
  end  
end
クマくん
クマくん

なんか色々難しいコードが書いてあるなぁ。

さめさん
さめさん

このfactory_botの作り方が初見だと難しいよね・・・

ちゃんと解説するから!

まずはやってほしいことをポイントにまとめるね。

itemのfactory_botの作成ポイント
  • テスト用の画像はRack::Test::UploadedFileモジュールで呼び出す
  • afterメソッドを使ってインスタンスの作成タイミングをコントロールする。
一緒に抑えて行こう!
テスト用の写真データ
さめさん
さめさん

テスト用の写真をまずは配置しようか。

spec/fixturesというディレクトリを作ってそこに

任意の写真のデータをおいてね!

クマちゃん
クマちゃん

おっけー!

置いたよ!

クマくん
クマくん

factory_botでそのまま文字列で指定しちゃダメなの?

一応imagesテーブルのphotoカラムはtext型になっているから適当な文字列で置いちゃってもいいんじゃないかなって思ってたんだけど。

さめさん
さめさん

参考リンクを見てほしいんだけど、carrierwaveで変換されたファイルをテストする時の呼び出し方法がcarrierwaveのgithubに書いてあるんだ。

さめさん
さめさん

作成するだけだったらシンプルにafterを使ってできるよとも書いてあったけどね。

 今回の場合はafterでネストしすぎちゃうから、、、

さめさん
さめさん

 Rack::Test::UploadedFileモジュールを使ってデータを渡してあげた方がいいと思う!

factory :image do
  photo   { Rack::Test::UploadedFile.new(File.join(Rails.root, "spec/fixtures/sample.png"), 'image/png') }
end  
afterメソッドを使ってインスタンスの作成タイミングをコントロール。
クマくん
クマくん

これが一番よくわからなかったんだよね・・・

さめさん
さめさん

分かるよ・・・

さめさん
さめさん

itemsとimagesテーブルの構成を考えてみると、一見ありえないことをやっているんだ。

それぞれを参照しあっているから、

①itemの情報を保存するときにはその前に対応するimageの情報が保存されていなければいけない。

②imageの情報を保存するときには対応するitemの情報が保存されていないといけない。

クマくん
クマくん

えっと・・・つまりどういうことだろう?

さめさん
さめさん

例えばだけど、

喧嘩しているカップルがいるとして、復縁するためには

・彼氏サイドは最初に彼女から謝ってくれたら謝罪する。

・彼女サイドは最初に彼氏から謝ってくれたら謝罪する。

っていう条件があるってことと一緒かな。

クマくん
クマくん

そんなの一生復縁できないじゃん!!!

さめさん
さめさん

そうなんだよ・・・

だからほぼ同時に謝罪するような設定にしなきゃいけないんだ。

クマくん
クマくん

そんな無茶苦茶な・・・

さめさん
さめさん

それをいい感じにやってくれるのがafterメソッドなんだ!

  factory :item do
   # 省略

    after(:build) do |item|
      item.images << FactoryBot.build(:image, item: item)
    end  
  end
さめさん
さめさん

データを保存する時って

①インスタンスを作成する。(new or build)

②インスタンスを保存する。(save)

の2段階あってね。

さめさん
さめさん

今回の場合だと、

item用のインスタンスを作成した後、image用のインスタンスを作成する。

※FactoryBot.build(:image, item: item)

さめさん
さめさん

そして、item.imagesという空の配列にimage用のインスタンスを格納しているんだ。

※item.images << FactoryBot.build(:image, item: item)

クマくん
クマくん

FactoryBot.build(:image, item: item)って何をやっているの?

さめさん
さめさん

これちょっとややこしいかな。

クマくんにもわかりやすく書くとこういうことかな。

FactoryBot.define do
  factory :item do
    name {"美味しいサラダ"}
    user { FactoryBot.create(:user)}

    after(:build) do |built_item|
      built_item.images << FactoryBot.build(:image, item: built_item)
    end  
  end


  factory :image do
    photo   { Rack::Test::UploadedFile.new(File.join(Rails.root, "spec/fixtures/sample.png"), 'image/png') }
  end  
end
クマくん
クマくん

一部のitemって名前がbuilt_itemって変わったね。

さめさん
さめさん

itemのインスタンスを作った後にそのインスタンスの名前をbuilt_itemにしてみたよ。

これで分かりやすくなったと思うだけど、どうかな?

クマくん
クマくん

なるほど。

itemっていうキーにさっき作ったbuilt_itemっていうインスタンスを渡していたんだね。

さめさん
さめさん

そうそう!!

ここまででやっとテスト用のデータを設定することができたね。

テスト実行

さめさん
さめさん

早速テストを実行してみよう。

$ bundle exec rspec
さめさん
さめさん

こんな感じになっていたらテストが通っている証だよ。

Finished in 0.00344 seconds (files took 0.6508 seconds to load)
1 example, 0 failures

終わりに

クマくん
クマくん

サメさん、ありがとう。

afterメソッドを使って二つのテーブルの情報を保存させるやり方マスターした気がするよ!

さめさん
さめさん

よかった。

beforeっていうメソッドもあるし、factory_botの作り方はあるからちょっとずつ使いこなしていけるといいね。

クマくん
クマくん

ちゃんと整理して使えるようになるぞ〜〜

今回学んだポイント
  • RSpecでどのようなテストをするか決めよう。
  • factory_botでテストデータを作ろう。
  • carrierwaveを使って画像を保存しているときはRack::Test::UploadedFileモジュールを使おう
  • afterメソッドを用いてテストインスタンスの保存に至る流れをコントロールしよう。
おさらいだよ!
クマくん
クマくん

Railsを勉強中だったらこの本がおすすめだよ!

おすすめ記事

よかったらシェアしてね!
  • URL Copied!
  • URL Copied!
目次
閉じる