関連する子モデルの情報を含んだフォームのバリデーションテスト(Rspec)


はじめに

スクールでフリマアプリ開発の際、出品フォームのバリデーションテストを担当しました。
その時、出品画像のバリデーションテストで苦労したので、復習も兼ねて記事に残そうと思います。

今回のケース

  • 商品名や商品説明を保存するテーブル(productsテーブル)と、出品画像を保存するテーブル(imagesテーブル)が分かれて存在している。
  • アソシエーションは、has_many :images と、belongs_to :product の関係である。
  • 1つのフォームから、複数のテーブルにデータを送信して保存する実装をしている。
  • Productモデルに、validates :images , presence: true を記載しており、画像が無いと商品を出品できないようになっている。

問題なのは、画像が無いと出品ができないため、
フォーム上に画像がアップロードされている状態を、テストで再現しなくてはいけません。
このような場合、どうやってテストすればいいのでしょか?

それについて記述していきます。

テストの方法

まずはfactory_botを使って、imageの情報を含まないProductモデルのインスタンスを準備してみます。

spec/factories/products.rb
FactoryBot.define do
  factory :product do  #画像なしバージョン
    id                {"1"}
    name              {"商品名"}
    detail            {"詳細"}
    category_id       {"686"}
    which_postage     {"1"}
    prefecture        {"1"}
    shipping_date     {"1"}
    price             {"1000"}
    size
    condition
    sending_method
    user
  end
end

今回注目して欲しいのはimageについてなので、それ以外の値については気にしなくて大丈夫です。

この状態で、とりあえず下記のテストを実行してみます。

spec/models/product_spec.rb
require 'rails_helper'
describe Product do
  describe '#create' do
    it "全ての必須項目が入力されている場合出品できる" do
      product = FactoryBot.build(:product)
      expect(product).to be_valid
    end
  end
end
ターミナル
Product
  #create
    全ての必須項目が入力されている場合出品できる (FAILED - 1)

Failures:

  1) Product#create 全ての必須項目が入力されている場合出品できる
     Failure/Error: expect(product).to be_valid
       expected #<Product id: 1, name: "商品名", detail: "詳細", shipping_date: 1, price: 1000, which_postage: 1, delivery_status: "出品中", prefecture: 1, created_at: nil, updated_at: nil, brand_id: nil, user_id: 1, size_id: 1, condition_id: 1, sending_method_id: 1, buyer_id: nil, category_id: 686> to be valid, but got errors: 商品画像を入力してください
     # ./spec/models/product_spec.rb:154:in `block (3 levels) in <top (required)>'

Finished in 0.63978 seconds (files took 5.52 seconds to load)
1 example, 1 failure

当然ですが、「商品画像を入力してください」と言われ、テストは失敗します。
product.imagesの中身が空っぽな訳です。
なんとかしてproduct.imagesへ画像の情報を入れてあげなければいけません。

その為に、imageのfactoryをproductとは別に準備します。

spec/factories/images.rb
FactoryBot.define do

  factory :image do
    image   {File.open("#{Rails.root}/spec/fixtures/test_image.png")}
    product
  end

end

imageの準備ができたら、spec/factories/products.rb を以下のように編集します。

spec/factories/products.rb
FactoryBot.define do
  factory :product do  #画像ありバージョン
    id                {"1"}
    name              {"商品名"}
    detail            {"詳細"}
    category_id       {"686"}
    which_postage     {"1"}
    prefecture        {"1"}
    shipping_date     {"1"}
    price             {"1000"}
    size
    condition
    sending_method
    user
    after(:build) do |product|                           #追記
      product.images << build(:image, product: product)  #追記
    end                                                  #追記
  end
end

3行だけ追記しました。

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

after(:build) do 〜 end の部分で、productのインスタンスが作成された直後に、imageのインスタンスを作成して、product.imagesの中にimageインスタンスの情報を入れてあげています。

これでフォーム上に画像がアップロードされた状態を再現することができました!
もう一度テストを実行してみます。

spec/models/product_spec.rb
require 'rails_helper'
describe Product do
  describe '#create' do
    it "全ての必須項目が入力されている場合出品できる" do
      product = FactoryBot.build(:product)
      expect(product).to be_valid
    end
  end
end
ターミナル
Product
  #create
    全ての必須項目が入力されている場合出品できる

Finished in 0.54654 seconds (files took 5.16 seconds to load)
1 example, 0 failures

今度は成功ですね!!

画像の枚数を変更したい場合

after(:build) do |product|
  product.images << build_list(:image, 10, product: product) #buildをbuild_listへ変更
end

build を build_listメソッドにすることで、画像の枚数も操作することができます。
上記の場合は、build_listの第2引数に10を渡している為、画像が10枚アップロードされた状態を再現しています。
境界値などをテストする場合は、build_listを使うといいと思います。

所感

テストに関しては、検索しても情報が少なく、苦労しました。
この記事が少しでも誰かの役に立てば嬉しいです。
初投稿ですので、間違い等ございましたら、ご指摘いただけると幸いです。
ここまで読んでいただき、ありがとうございました!!

参考にさせていただいた記事

accepts_nested_attributes_forで作成するpresence:trueを付けた子モデルに対するモデルのバリデーションチェック~RSpec~
FactoryBotとCarrierWaveを使ってRSpecに画像ファイルアップロードのテストを通す