11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FactoryGirlで固定されたデータを扱う

11
Posted at

はじめに

Webアプリケーションを作っていると、ユーザー入力値ではなく固定されたマスターデータをDBで扱うことがしばしばあると思います。例えば都道府県など。

prefectures.rb
# == Schema Information
#
# Table name: prefectures # 都道府県
#
#  id         :integer          not null, primary key
#  name       :string(255)      not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class Prefecture < ApplicationRecord
  has_many :cities
  has_many :addresses, through: :cities
end

このモデルの FactroryGirl の設定を考えます。一般的なモデルにならうと

prefectures.rb
FactoryGirl.define do
  factory :prefecture do
    name '東京都'
  end
end

となりますが、これだとテストを実行する度に東京都がたくさん生成されます。しかも、場合によっては Prefecture のインスタンス数が47を超えます。現実的でありませんね。かといって

prefectures.rb
FactoryGirl.define do
  factory :prefecture do
    id 13
    name '東京都'
  end
end
# ActiveRecord::RecordNotUnique: Mysql2::Error: Duplicate entry '13' for key 'PRIMARY': INSERT INTO `prefectures` (`id`, `name`, `created_at`, `updated_at`) VALUES (13, '東京都', '2017-10-26 10:31:00', '2017-10-26 10:31:00')
# from /Users/***/.rbenv/versions/2.3.3/lib/ruby/gems/2.3.0/gems/mysql2-0.4.5/lib/mysql2/client.rb:120:in `_query'

としてしまうと今度はDBの一意性制約に引っかかって怒られます。

initialize_with を使う

FactoryGirl が用意している initialize_with メソッドを使用すると、何回 buildcreate を呼んでも特定のインスタンス (テスト用DBの同一のレコード) を返す Factory が定義できます。

prefectures.rb
FactoryGirl.define do
  factory :prefecture do
    id 13
    name '東京都'

    initialize_with do
      Prefecture.find_or_initialize_by(
        id: 13,
        name: '東京都')
    end
  end
end

find_or_initialize_by を使っているので、これはデータベースが真っさらな状態でも期待通り動きます。

しかも、 association でつながっている他のFactoryを createbuild した時、SQLの CREATE ではなく SELECT が走るので、SQLの実行時間ひいてはテストの実行時間を削減できることが期待できます。

[7] pry(main)> FactoryGirl.create(:city)
  Prefecture Load (0.4ms)  SELECT  `prefectures`.* FROM `prefectures` WHERE `prefectures`.`id` = 13 AND `prefectures`.`region_id` = 3 AND `prefectures`.`name` = '東京都' LIMIT 1
  SQL (27.7ms)  INSERT INTO `cities` (`prefecture_id`, `name`, `created_at`, `updated_at`) VALUES (13, '新宿区', '2017-10-26 10:43:52', '2017-10-26 10:43:52')
   (2.8ms)  COMMIT

prefectures テーブルに対する CREATE 文が走っていないことが分かります。

注意点

ただし、これを使うと本当に他の値を使いたくてもできなくなるという欠点があります。データ作成時に CREATE ではなく UPDATE が走るためです。

[5] pry(main)> FactoryGirl.create(:prefecture, id: 14, name: '神奈川県')
  SQL (5.1ms)  UPDATE `prefectures` SET `id` = 14, `name` = '神奈川県', `updated_at` = '2017-10-26 10:39:14' WHERE `prefectures`.`id` = 13
ActiveRecord::StatementInvalid: Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint fails (`***_development`.`cities`, CONSTRAINT `fk_rails_cc74ecd368` FOREIGN KEY (`prefecture_id`) REFERENCES `prefectures` (`id`)): UPDATE `prefectures` SET `id` = 14, `name` = '神奈川県', `updated_at` = '2017-10-26 10:39:14' WHERE `prefectures`.`id` = 13

外部キー制約に引っかかっています。

対策

この場合は Factory を使わず ActiveModel.create を使います。

[11] pry(main)> Prefecture.create(name: '神奈川県')
   (0.2ms)  BEGIN
  SQL (9.0ms)  INSERT INTO `prefectures` (`name`, `created_at`, `updated_at`) VALUES ('神奈川県', '2017-10-26 10:46:44', '2017-10-26 10:46:44')
   (6.7ms)  COMMIT

これは新規Factoryつまりダミーデータが非常に作りにくくなる事実を示しています。カラムが多いレコードに対してここで紹介した方法を使うのは得策でないでしょう。

まとめ

  • initialize_withfind_or_initialize_by を使うと、固定されたデータから参照する形式のFactoryが定義できる。
  • しかし新規ダミーデータを作成するのは大変になる。
  • 使いどころが肝心

以上になります。

11
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?