factoryBotの{ }はどういう挙動になるのか

こんにちは!kossyです!




アウトプットが大事だと頭ではわかっていながら、
AmazonPrimeVideoにどっぷりの正月休みでした、、、笑

もう新年明けて仕事も始まっているので、
気持ちを切り替えて粛々とブログを更新していきます。




さて、今回はRailsのテストでテストデータを簡単に生成できるGem、
factoryBotでファクトリファイルを記述する際に使用する、
{}を使った記法でどういう挙動になるのか、ブログに残してみたいと思います。




ブロックで囲むと、遅延評価される

結論から言うとタイトル通りになります。
これだけでわかれば苦労しないので、もう少し解説します。



遅延評価とは、今回のケース(FactoryBotを使う)で言えば、テストデータを生成する時に評価されると言う意味になります。
ブロック構文を使わないと、rspecの立ち上げ時に評価されますが、
{}を使って値を定義すれば、FactoryBotを使ってインスタンスを生成しようとした時に、
値が評価されます。



それでもわからねぇ()

上記のことを上司から説明されましたが、実際の動きを確認しないと腑に落ちません。


ってことで、コンソールで試してみます。

$ rails c test --sandbox

このコマンドで、テスト環境のコンソールをDBの中身を変更させずにいじることができます。
(正確に言うとexit時にrollbackが走って変更がリセットされる)
別にsandboxいらないんですけどね、、、笑


早速試してみます。

factories

# user.rb

FactoryBot.define do
  factory :user do
    name                  { Faker::Name.name }
    email                 { Faker::Internet.email }
    password              { 'test1234' }
    password_confirmation { 'test1234' }
    role                  { 0 }
    created_at              Time.now # <- ブロック無し
    updated_at             Time.now # <- ブロック無し
  end
end



# review.rb

FactoryBot.define do
  factory :review do
    bookname { 'Perfect Ruby' }
    content  { 'テストだよ' }
    language { 'Ruby' }
    level    { '初級' }
    created_at { Time.now } #<- ブロックあり
    updated_at { Time.now } #<- ブロックあり
    user
  end
end

これでコンソールで試してみます。

$ rails c test --sandbox


# 以下はログ

[1] pry(main)> FactoryBot.create :user
   (0.3ms)  SAVEPOINT active_record_1
  User Exists (0.7ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'danellekuhlman@goodwinfay.com' LIMIT 1
  User Exists (0.4ms)  SELECT  1 AS one FROM `users` WHERE `users`.`name` = BINARY '増田 美緒' LIMIT 1
  User Exists (0.5ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'danellekuhlman@goodwinfay.com' LIMIT 1
  SQL (0.5ms)  INSERT INTO `users` (`email`, `encrypted_password`, `created_at`, `updated_at`, `name`) VALUES ('danellekuhlman@goodwinfay.com', '$2a$04$cKY02.d3veqIt4Clqc7fA.m1NhAYarMjsEbGxbK9H24urUcyVziQq', '2019-01-07 21:17:31', '2019-01-07 21:17:31', '増田 美緒')
   (0.3ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 10, email: "danellekuhlman@goodwinfay.com", created_at: "2019-01-07 12:17:31", updated_at: "2019-01-07 12:17:31", name: "増田 美緒", role: "user">
[2] pry(main)> FactoryBot.create :user
   (0.3ms)  SAVEPOINT active_record_1
  User Exists (0.5ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'shannonveum@greenfelderbode.biz' LIMIT 1
  User Exists (0.3ms)  SELECT  1 AS one FROM `users` WHERE `users`.`name` = BINARY '林 莉子' LIMIT 1
  User Exists (0.3ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'shannonveum@greenfelderbode.biz' LIMIT 1
  SQL (0.7ms)  INSERT INTO `users` (`email`, `encrypted_password`, `created_at`, `updated_at`, `name`) VALUES ('shannonveum@greenfelderbode.biz', '$2a$04$vr6dTtCBjX5ZHfLr1scLQumF.Fe3JbZBPo6Sg8rdcPOVeJ/LZLhOm', '2019-01-07 21:17:31', '2019-01-07 21:17:31', '林 莉子')
   (0.3ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 11, email: "shannonveum@greenfelderbode.biz", created_at: "2019-01-07 12:17:31", updated_at: "2019-01-07 12:17:31", name: "林 莉子", role: "user">
[3] pry(main)> FactoryBot.create :user
   (0.2ms)  SAVEPOINT active_record_1
  User Exists (0.3ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'boyce@corkery.io' LIMIT 1
  User Exists (0.3ms)  SELECT  1 AS one FROM `users` WHERE `users`.`name` = BINARY '藤本 大樹' LIMIT 1
  User Exists (0.3ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'boyce@corkery.io' LIMIT 1
  SQL (0.3ms)  INSERT INTO `users` (`email`, `encrypted_password`, `created_at`, `updated_at`, `name`) VALUES ('boyce@corkery.io', '$2a$04$DLgUxLniJfVlU9d/0ZtF/ev1W3q6yzXn.C85OMB5wwKytp8R6sPoe', '2019-01-07 21:17:31', '2019-01-07 21:17:31', '藤本 大樹')
   (0.2ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 12, email: "boyce@corkery.io", created_at: "2019-01-07 12:17:31", updated_at: "2019-01-07 12:17:31", name: "藤本 大樹", role: "user">

注目してほしいのは、created_atの値です。
どのレコードも、'2019-01-07 21:17:31'で作成されています。

次はreviewの方をコンソールで試します。

$ rails c test --sandbox

[4] pry(main)> FactoryBot.create :review
   (0.6ms)  SAVEPOINT active_record_1
  User Exists (1.2ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'zoila@hane.io' LIMIT 1
  User Exists (0.5ms)  SELECT  1 AS one FROM `users` WHERE `users`.`name` = BINARY '杉山 大地' LIMIT 1
  User Exists (0.4ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'zoila@hane.io' LIMIT 1
  SQL (0.5ms)  INSERT INTO `users` (`email`, `encrypted_password`, `created_at`, `updated_at`, `name`) VALUES ('zoila@hane.io', '$2a$04$NGsicKSVqrz0I2CfikbozuLLcAZGlgYkiQmLu0dzzj2uKXOjKE/Vu', '2019-01-07 21:17:31', '2019-01-07 21:17:31', '杉山 大地')
   (0.2ms)  RELEASE SAVEPOINT active_record_1
   (0.3ms)  SAVEPOINT active_record_1
  SQL (30.9ms)  INSERT INTO `reviews` (`user_id`, `bookname`, `content`, `language`, `level`, `created_at`, `updated_at`) VALUES (13, 'Perfect Ruby', 'テストだよ', 'Ruby', '初級', '2019-01-07 21:24:12', '2019-01-07 21:24:12')
   (0.2ms)  RELEASE SAVEPOINT active_record_1
=> #<Review:0x00007fc5afcfaf18
 id: 3,
 user_id: 13,
 bookname: "Perfect Ruby",
 content: "テストだよ",
 language: "Ruby",
 level: "初級",
 created_at: Mon, 07 Jan 2019 21:24:12 JST +09:00,
 updated_at: Mon, 07 Jan 2019 21:24:12 JST +09:00,
 image: nil>
[5] pry(main)> FactoryBot.create :review
   (0.3ms)  SAVEPOINT active_record_1
  User Exists (0.6ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'bev@toy.name' LIMIT 1
  User Exists (0.4ms)  SELECT  1 AS one FROM `users` WHERE `users`.`name` = BINARY '前田 杏' LIMIT 1
  User Exists (0.4ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'bev@toy.name' LIMIT 1
  SQL (0.4ms)  INSERT INTO `users` (`email`, `encrypted_password`, `created_at`, `updated_at`, `name`) VALUES ('bev@toy.name', '$2a$04$jWBf1xZ3Mq3frPFPZwPrfeTAtnxbL2r1byrkAN2VfurltZppKtVrK', '2019-01-07 21:17:31', '2019-01-07 21:17:31', '前田 杏')
   (0.3ms)  RELEASE SAVEPOINT active_record_1
   (0.2ms)  SAVEPOINT active_record_1
  SQL (0.5ms)  INSERT INTO `reviews` (`user_id`, `bookname`, `content`, `language`, `level`, `created_at`, `updated_at`) VALUES (14, 'Perfect Ruby', 'テストだよ', 'Ruby', '初級', '2019-01-07 21:24:19', '2019-01-07 21:24:19')
   (0.2ms)  RELEASE SAVEPOINT active_record_1
=> #<Review:0x00007fc5b51029a8
 id: 4,
 user_id: 14,
 bookname: "Perfect Ruby",
 content: "テストだよ",
 language: "Ruby",
 level: "初級",
 created_at: Mon, 07 Jan 2019 21:24:19 JST +09:00,
 updated_at: Mon, 07 Jan 2019 21:24:19 JST +09:00,
 image: nil>
[6] pry(main)> FactoryBot.create :review
   (0.2ms)  SAVEPOINT active_record_1
  User Exists (0.4ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'ferdinandyundt@satterfieldhalvorson.com' LIMIT 1
  User Exists (0.3ms)  SELECT  1 AS one FROM `users` WHERE `users`.`name` = BINARY '野口 悠' LIMIT 1
  User Exists (0.3ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = BINARY 'ferdinandyundt@satterfieldhalvorson.com' LIMIT 1
  SQL (0.4ms)  INSERT INTO `users` (`email`, `encrypted_password`, `created_at`, `updated_at`, `name`) VALUES ('ferdinandyundt@satterfieldhalvorson.com', '$2a$04$WovEde/7WJawJLYJBkqbRuElisVtJR.yW96Rqvk95AUqsRp1HBfq6', '2019-01-07 21:17:31', '2019-01-07 21:17:31', '野口 悠')
   (0.2ms)  RELEASE SAVEPOINT active_record_1
   (0.2ms)  SAVEPOINT active_record_1
  SQL (0.4ms)  INSERT INTO `reviews` (`user_id`, `bookname`, `content`, `language`, `level`, `created_at`, `updated_at`) VALUES (15, 'Perfect Ruby', 'テストだよ', 'Ruby', '初級', '2019-01-07 21:24:21', '2019-01-07 21:24:21')
   (0.2ms)  RELEASE SAVEPOINT active_record_1
=> #<Review:0x00007fc5b3e954f0
 id: 5,
 user_id: 15,
 bookname: "Perfect Ruby",
 content: "テストだよ",
 language: "Ruby",
 level: "初級",
 created_at: Mon, 07 Jan 2019 21:24:21 JST +09:00,
 updated_at: Mon, 07 Jan 2019 21:24:21 JST +09:00,
 image: nil>

めっちゃ長いですが、見るべき所はこちらもcreated_atだけです。

'2019-01-07 21:24:12'
'2019-01-07 21:24:19'
'2019-01-07 21:24:21'

3つのレコード全ての値が異なっています。







これが遅延評価だ!!

ブロックで囲まなかった方は、コンソールの立ち上げ時にfactoryの値が評価されて(Time.nowの値が決まる)いますが、
ブロックで囲ったTime.nowの方は、factoryを呼び出す(レコードを生成する)たびにTime.nowが行われているのがわかります。



コンソールで試すと理解が進みますね。



参考にさせていただいた記事
RSpecにおけるFactoryGirlの使い方まとめ - Qiita
FactoryGirlチートシート - Qiita