#はじめに
rspecのTagのmodelテストをする際、中間テーブル(post_tag)を介したやり方に苦戦したため、
factory_botを用いて、多対多関係 (has_many through) のテスト作成方法をご説明致します。
##テーブル
post
とtag
の間にpost_tag
テーブルがある状態です。
#到達点
以下の2点を達成する
・中間テーブルを介したmodelテストのFactoryBotを理解する
・中間テーブルを介したmodelテストの記述方法を理解する
##流れ
① 各モデルのvalidatesを確認
② FactoryBotの記述
③ modelテストの記述
#① 各モデルのvalidatesを確認
class Post < ApplicationRecord
has_many :post_tags, dependent: :destroy
has_many :tags, through: :post_tags
validates :title,
presence: true,
length: { maximum: 60 }
validates :body,
presence: true,
length: { maximum: 2000 }
end
class Tag < ApplicationRecord
has_many :post_tags, dependent: :destroy
has_many :posts, through: :post_tags
validates :name, presence: true, length: { maximum: 50 }
end
class PostTag < ApplicationRecord
belongs_to :post
belongs_to :tag
validates :post_id, presence: true
validates :tag_id, presence: true
#② FactoryBotの記述
FactoryBot.define do
factory :post do
sequence(:title) { |n| "title-#{n}" }
sequence(:body) { |n| "body-#{n}" }
after(:create) do |post|
create_list(:post_tag, 1, post: post, tag: create(:tag))
end
end
end
after(:create)
を使用することで、post生成後に、tagとpost_tagが生成されます。
FactoryBot.define do
factory :tag do
sequence(:name) { |n| "tag-#{n}" }
end
end
sequence
でユニークnameを生成できます。
FactoryBot.define do
factory :post_tag do
association :post
association :tag
end
end
association :post
association :tag
とすることで、
post_tagのmodelテストにおいて
let(:post_tag) { create(:post_tag) }
と記述するだけで
postとtagも生成できます。
ただし、associationはhas_many
側(今回の場合,post,tag)では記述せず、
belong_to
側でのみ使用しましょう。
#③ modelテストの記述
RSpec.describe Post, type: :model do
let(:post) { create(:post) }
it "タイトル、本文、user_idがある場合、有効であること" do
expect(post).to be_valid
end
it "user_idがない場合、無効であること" do
post.user_id = nil
expect(post).to be_invalid
end
describe "タイトル" do
it "タイトルがない場合、無効であること" do
post.title = nil
expect(post).to be_invalid
expect(post.errors[:title]).to include("を入力してください")
end
context "タイトルが60文字以下の場合" do
it "有効であること" do
post.title = "1" * 60
expect(post).to be_valid
end
end
context "タイトルが61文字以上の場合" do
it "無効であること" do
post.title = "1" * 61
expect(post).to be_invalid
end
end
end
describe "本文" do
it "本文がない場合、無効であること" do
post.body = nil
expect(post).to be_invalid
expect(post.errors[:body]).to include("を入力してください")
end
context "本文が2000文字以下の場合" do
it "有効であること" do
post.body = "1" * 2000
expect(post).to be_valid
end
end
context "本文が2001文字以上の場合" do
it "無効であること" do
post.body = "1" * 2001
expect(post).to be_invalid
end
end
end
end
RSpec.describe Tag, type: :model do
let(:tag) { create(:tag) }
describe "name" do
it "タグ名がある場合、有効であること" do
expect(tag).to be_valid
end
it "タグ名がない場合、無効であること" do
tag.name = nil
expect(tag).to be_invalid
expect(tag.errors[:name]).to include("を入力してください")
end
context "タグ名が50文字以下の場合" do
it "有効であること" do
tag.name = "1" * 50
expect(tag).to be_valid
end
end
context "タグ名が51文字以上の場合" do
it "無効であること" do
tag.name = "1" * 51
expect(tag).to be_invalid
expect(tag.errors[:name]).to include("は50文字以内で入力してください")
end
end
end
end
RSpec.describe PostTag, type: :model do
let(:post_tag) { create(:post_tag) }
it "post_idとtag_idがある場合、有効であること" do
expect(post_tag).to be_valid
end
it "post_idがない場合、無効であること" do
post_tag.post_id = nil
expect(post_tag).to be_invalid
end
it "tag_idがない場合、無効であること" do
post_tag.tag_id = nil
expect(post_tag).to be_invalid
end
end
association
によってlet(:post_tag) { create(:post_tag) }
が一文で済みました。
なお、itやcontext内の文章は、英語だとスペルミス等が出る可能性があるため、基本日本語にしております。
間違い等がありましたらご指摘の方よろしくお願いします。
#参考記事
FactoryBot(FactoryGirl)チートシート
factory_girl で最低限知っておきたい4つの使い方
FactoryBot(旧FactoryGirl)で関連データを同時に生成する方法いろいろ