はじめに

困ったなぁ。




クマくん、どうしたの?
やけに深刻そうな顔して。




実は、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 | |
name | itemの名前 |
user_id | ユーザーの参照 |
created_at | 作成日 |
updated_at | 更新日 |




imagesテーブルだよ。
id | |
item_id | itemsテーブルとの参照 |
photo | 写真のデータ保存 |
created_at | 作成日 |
updated_at | 更新日 |




usersテーブルだよ。
id | |
nickname | ニックネーム |
メールアドレス | |
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の作り方が初見だと難しいよね・・・
ちゃんと解説するから!
まずはやってほしいことをポイントにまとめるね。
- テスト用の画像は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を勉強中だったらこの本がおすすめだよ!