Rspecでテストコード を書く③ 〜2つのテーブルを同時に保存編〜

TECH::CAMP卒業まで残すところあと2週間となりました。

 

入学当初はできなかった事が、継続する事で本当にいろいろできるようになったと思っています。

 

できるようになったことは、できるだけアウトプット!

ということで、本日はテストコード の記事3つ目を書きたいと思います。

 


 

今回の記事の内容

テストコード を描けるようになった後でも結構詰まってしまった、
2つのテーブルを同時保存するときのテストです。
メルカリのクローンサイトを作っているのですが、商品の出品時に、下記の2テーブルにデータを同時に保存しなければなりません。

  • itemsテーブル(カテゴリーも保存しなければならない)
  • imagesテーブル(itemsテーブル保存時に同時に保存しなければならない)

カテゴリーはgem 'ancestry'を使用しており、
親、子、孫カテゴリーを作成しているのですが、
itemsテーブルには必ず孫カテゴリーを保存するような設計にしているので、
そこも実現したい。

参考記事

下記記事を参考にさせていただきました!!!
【Rails、RSpec】Ancestryを用いている時のFactoryBotの作り方 - Qiita

世界一わかりやすい!!複数画像投稿時のRSpecテストの書き方 | 目指せ、スーパーエンジニア


 

作業の前に(前回の復習)

  • gemのインストール
group :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'faker', "~> 2.8"
gem 'rails-controller-testing' #コントローラーのテストをする場合
end
$ rails g spec:install
  • .rspec ファイルへの追記
--format documentation
  • rails.helper.rb ファイルへの追記
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods #ここを追記
#省略
end

DB design

DB設計は下記の通り。アソシエーションも記載しています。

  • itemsテーブル
Column Type Options
------ ---- -------
name string null: false
price integer null :false
content text null :false
category_id references null: false, foreign_key: true
condition integer null: false
brand string
size string
preparation_day integer null: false
postage_type integer null: false
postage_payer integer null: false
seller_id references null: false, foreign_key: true
buyer_id references foreign_key: true
prefecture_id(active_hash) integer null: false
  • Association
    • belongs_to :user
    • belongs_to :category
    • has_many :images
  • imagesテーブル
Column Type Options
------ ---- -------
item_id references null: false, foreign_key: true
image_url string null: false
  • Association
    • belongs_to :item
  • categoriesテーブル
Column Type Options
------ ---- -------
name string null: false
ancestry string
  • Association
    • has_many :items

itemモデルのバリデーション

  validates_associated :images
  validates :name, presence: true, length: { maximum: 40 }
  validates :content, presence: true, length: { maximum: 1000 }
  validates :category_id, presence: true
  validates :condition, presence: true
  validates :postage_payer, presence: true
  validates :postage_type, presence: true
  validates :prefecture_id, presence: true
  validates :preparation_day, presence: true
  validates :price, presence: true
  validates :seller_id, presence: true
  validate  :images_number                                   //カスタムバリデーション。画像は1個以上10個以下存在しなければならない。
 
  def images_number
    errors.add(:images, "を1つ以上指定してください") if images.size < 1
    errors.add(:images, "は10個までです") if images.size > 10
  end 

カテゴリーのFactoryBot、rspecの書き方

カテゴリーは、一旦親カテゴリーをFactoryBotで作ってやります。
(ancestryカラムがnilなのが親カテゴリ)

FactoryBot.define do
  factory :category do
    name              {"category"}
    ancestry          {nil}
  end
end
||<<

次にitemsテーブルのFactoryBotのなかで、ancestryで使えるメソッドを使用して
孫カテゴリを作り、保存するという動作を書きます。

>|ruby|
FactoryBot.define do

  factory :item do
    name                        {"商品"}
    content                    {"good"}
    brand                       {"NIKE"}
    condition                    {1}
    postage_payer                 {1}
    postage_type                  {1}
    prefecture_id                 {1}
    preparation_day               {1}
    price                        {1}
    buyer_id                      {}              //出品時は空なので空にしています
    after(:build) do |item|
      parent_category = create(:category)
      child_category = parent_category.children.create(name: "child")      
      grand_child_category = child_category.children.create(name: "grandchild")

      item.category_id = grand_child_category.id

    end
  end
end

ここで初めて使ったのが、after(:build) という書き方。
rspecでitemモデルをbuildした後に加える動作を指示できるようです。
親カテゴリモデルを作成
   ↓
子カテゴリモデルを作成
   ↓
孫カテゴリモデルを作成
   ↓
孫カテゴリのidをitemsテーブルのcategory_idカラムに保存

という流れを記載しています。


imagesテーブルの処理の仕方

imagesテーブルのFactoryBotファイルは作りません。
imagesテーブルの場合は、ダミーの画像をspec/fixturesディレクトリに保存しておき、
Rack::Test::UploadedFileモジュールで呼び出してやります。
その上で、カテゴリーと同様、itemモデルをを(:build)するときに、imageモデルも(:build)し、
item.imagesという配列に入れてやるという処理をします。

FactoryBot.define do

  factory :item do
    name                        {"商品"}
    content                    {"good"}
    brand                       {"NIKE"}
    condition                    {1}
    postage_payer                 {1}
    postage_type                  {1}
    prefecture_id                 {1}
    preparation_day               {1}
    price                        {1}
    buyer_id                      {}                 //出品時は空なので空にしています
    after(:build) do |item|
      parent_category = create(:category)
      child_category = parent_category.children.create(name: "hello")      
      grand_child_category = child_category.children.create(name: "world")

      item.category_id = grand_child_category.id

      item.images << FactoryBot.build(:image, item_id: item.id)
      
    end
  end

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

これで準備完了!!

テストコード

カテゴリーと画像関連以外は省略しています。

require 'rails_helper'
describe Item do
  describe "#create" do
    let(:user) { create(:user) }
    let(:item) {build(:item, seller_id: user.id)}      //seller_idが空やでとエラーが出たので常に足すようにしています。

  it "category_idがないと出品できないこと" do
      item.category_id = nil
      item.valid?
      expect(item.errors[:category_id]).to include("を入力してください")
    end

  it "imageがないと出品できないこと" do  
      item.images = []                                                           //item.imagesの配列が空であるということにしています。
      item.valid?
      expect(item.errors[:images]).to include("を1つ以上指定してください")
    end

  it "imageが10を超えると出品できないこと" do
      10.times {|num|
        item.images << build(:image, item_id: item.id)  //itemsモデルのFactoryBotで行った配列への画像挿入を、追加で10回行っています。
      }
      item.valid?
      expect(item.errors[:images]).to include("は10個までです")
    end
  end

これで無事にテストが通りました・・・!!


まとめ

テストをする事で、つけ忘れているバリデーション に気づく、なんていう
順番が逆じゃないかと言われそうなこともありました。
ですが、つけ忘れたバリデーション に気づくのも、きっとテストコード をかく役割なんじゃないかと思っています!
実際、最初にテストコード を書いて、それが通るようなコードを書くという方の記事を見つけました。

このコードをメンターさんに見てもらった時に、
ここまで書ける人は受講生の中でもなかなかいませんよ〜と言っていただき、
苦労が報われた気がして泣きそうになりました( ; ; )

最初わからない事でも、諦めずに頑張ればできるようになる!
これが本当にプログラミングの魅力・楽しみであると思います。
さらに世の中に役立つ物を作れれば、最高ですね!!!
卒業間近ですが、これからも楽しむことを忘れずに学習を続けていきたいと思います。