サンプルデータbuild、createどちらを使うか

概要、目的

サンプルデータの使い分け
テスト作成時に手こずったのでアウトプットも兼ねて

前提

使用するアプリのログイン機構はsorceryを使用
テストはrspec
FactoryBotを使用

今回使用するコード

# spec/factories/users.rb

FactoryBot.define do
  factory :user do
    sequence(:email){|n| "test@#{n}example.com" }
    password { 'password' }
    password_confirmation { 'password' }
  end
end
_____________________________________________________________________
# spec/features/users_spec.rb

RSpec.feature "Users", type: :feature do
  describe 'ログイン前' do

    # 変化させる箇所 create,buildした時の違い
    # ①let(:user) { create(:user) }
    # ②let(:user) { build(:user) }

    context 'フォームの入力値が正常' do
      it  'ユーザーの新規登録作成が成功する' do
        visit root_path
        click_link 'SignUp'
        fill_in 'Email', with: user.email
        fill_in 'Password', with: user.password
        fill_in 'Password confirmation', with: user.password_confirmation
        
        # ③byegug デバック時に使用

        click_button 'SignUp'
        expect(page).to have_content 'User was successfully created.'
      end
    end
  end
end

実現したいこと

図1のフォームが正常に動作し、ログインできるかのテスト

新規登録フォームが表示されています.
図1.新規登録フォーム

create,buildの使い分け

今回はサンプルデータをcreate,buildしてみてどのような違いがあるか、試していきます.

createしてテストすると...

結論から言うとcreateではテストはパスしません.
最初私はcreateしたデータで、テスト項目に当てはめればいいと思い、テストを作成しました.
今後テストを書いていく上で、同じ失敗を繰り返さないためにもアウトプットします.
上のコード①を有効にしてテストを実行します.

テスト結果

 $ bundle exec rspec

1) Users ログイン前 フォームの入力値が正常 ユーザーの新規登録作成が成功する
     Failure/Error: expect(page).to have_content 'User was successfully created.'
       expected to find text "User was successfully created." in "Login SignUp\nSignUp\n3 errors prohibited this user from being saved:\nPassword is too short (minimum is 3 characters) Password confirmation can't be blank Email has already been taken\nEmail\nPassword\nPassword confirmation\nBack"
     # ./spec/features/users_spec.rb:15:in `block (4 levels) in <main>'

Finished in 0.44038 seconds (files took 1.64 seconds to load)
18 examples, 1 failure, 8 pending

Failed examples:

rspec ./spec/features/users_spec.rb:7 # Users ログイン前 フォームの入力値が正常 ユーザーの新規登録作成が成功する

原因

どうなっているか上のコード③を有効にして再度テストを実行します.
userそのものは存在してるようです.

(byebug) user
#<User id: 1, email: "test@1example.com", crypted_password: "$2a$04$gk9IxtIsYvQIjxcluZv8R.iEycIUUU0DzF6EJMu62XC...", salt: "vD43wtyV-oauxVTaFAT_", created_at: "2021-02-26 02:49:46", updated_at: "2021-02-26 02:49:46">

各カラムに値があるか見ていきます.

(byebug) user.email
"test@1example.com"
(byebug) user.password
nil
(byebug) user.password_confirmation
nil
(byebug)

デバックの結果passwordカラムの値がnilのため、テスト時にnilの値が入力されたためにユーザー登録ができないことがわかります.
エラーメッセージを再度見直すと、パスワードが空だと言われています.

1) Users ログイン前 フォームの入力値が正常 ユーザーの新規登録作成が成功する
     Failure/Error: expect(page).to have_content 'User was successfully created.'
       expected to find text "User was successfully created." in "Login SignUp\nSignUp\n3 errors prohibited this user from being saved:\nPassword is too short (minimum is 3 characters) Password confirmation can't be blank Email has already been taken\nEmail\nPassword\nPassword confirmation\nBack"
     # ./spec/features/users_spec.rb:15:in `block (4 levels) in <main>'

ではなぜpasswordが空なのでしょうか?

コンソールでcreateした時と、buildした時の違いを見てます.

#create時

[1] pry(main)> create_user=FactoryBot.create(:user)
   (0.1ms)  SAVEPOINT active_record_1
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "test@1example.com"], ["LIMIT", 1]]
  User Create (2.1ms)  INSERT INTO "users" ("email", "crypted_password", "salt", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["email", "test@1example.com"], ["crypted_password", "$2a$10$FooeA7t.Wprve0Z7JhgZsuryfRypjCT7dcIUjJzSIwWvronJ7Q2ue"], ["salt", "_uQ2mDDruScW1H6sxDy7"], ["created_at", "2021-02-26 03:20:14.766456"], ["updated_at", "2021-02-26 03:20:14.766456"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User:0x00007fa18bb05610
 id: 7,
 email: "test@1example.com",
 crypted_password:
  "$2a$10$FooeA7t.Wprve0Z7JhgZsuryfRypjCT7dcIUjJzSIwWvronJ7Q2ue",
 salt: "_uQ2mDDruScW1H6sxDy7",
 created_at: Fri, 26 Feb 2021 03:20:14 UTC +00:00,
 updated_at: Fri, 26 Feb 2021 03:20:14 UTC +00:00>

#build時
[4] pry(main)> build_user=FactoryBot.build(:user)
=> #<User:0x00007fa18b341b40
 id: nil,
 email: "test@2example.com",
 crypted_password: nil,
 salt: nil,
 created_at: nil,
 updated_at: nil>

create時にはcrypted_passwod、saltの値が入っていて、build時にはnilということがわかります.
次にFactoryBotで定義されている値が入っているか確かめてます.
FactoryBotのコード(再掲載)

FactoryBot.define do
  factory :user do
    sequence(:email){|n| "test@#{n}example.com" }
    password { 'password' }
    password_confirmation { 'password' }
  end
end

# ここからコンソール上のコードと比べながら
# create_user = FactryBot.create(:user)
# build_user = FactryBot.build(:user)

# create時
[7] pry(main)> create_user.email
=> "test@1example.com"
[8] pry(main)> create_user.password
=> nil
[9] pry(main)> create_user.password_confirmation
=> nil

#build時
[10] pry(main)> build_user.email
=> "test@2example.com"
[11] pry(main)> build_user.password
=> "password"
[12] pry(main)> build_user.password_confirmation
=> "password"
[13] pry(main)>

テスト失敗時デバックした時と同じように、createするとpasswod,password_confirmationがnilになっています.
反対にbuildではFactryBotで定義した通りに、password,password_confirmationカラムに値が入っています.
create時はpasword,password_confirmationがnilになり、build時には値が入っていることがわかりました.
ではなぜcreate時はnilになるのでしょうか?
再度こちらのコードを見てみましょう.

#create時

[1] pry(main)> create_user=FactoryBot.create(:user)
   (0.1ms)  SAVEPOINT active_record_1
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "test@1example.com"], ["LIMIT", 1]]
  User Create (2.1ms)  INSERT INTO "users" ("email", "crypted_password", "salt", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["email", "test@1example.com"], ["crypted_password", "$2a$10$FooeA7t.Wprve0Z7JhgZsuryfRypjCT7dcIUjJzSIwWvronJ7Q2ue"], ["salt", "_uQ2mDDruScW1H6sxDy7"], ["created_at", "2021-02-26 03:20:14.766456"], ["updated_at", "2021-02-26 03:20:14.766456"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User:0x00007fa18bb05610
 id: 7,
 email: "test@1example.com",
 crypted_password:
  "$2a$10$FooeA7t.Wprve0Z7JhgZsuryfRypjCT7dcIUjJzSIwWvronJ7Q2ue",
 salt: "_uQ2mDDruScW1H6sxDy7",
 created_at: Fri, 26 Feb 2021 03:20:14 UTC +00:00,
 updated_at: Fri, 26 Feb 2021 03:20:14 UTC +00:00>

#build時
[4] pry(main)> build_user=FactoryBot.build(:user)
=> #<User:0x00007fa18b341b40
 id: nil,
 email: "test@2example.com",
 crypted_password: nil,
 salt: nil,
 created_at: nil,
 updated_at: nil>

create時にはpassword,password_confirmationカラムの値がnilに対して,crypted_passwordというカラムに値が代入されています.
これはpasswordをハッシュ化した値が代入されています.
ハッシュ化することで、悪意を持ったユーザーからpasswordを盗まれないようにするためです.
つまりnilになるのはsorceryのコードが関係しています.
corceryではユーザーが入力したpassword,password_confirmationカラムの値を同一か確認し、さらにpasswordをハッシュ化しcrypted_passwordに保存しています.
crypted_passwordに値が代入されたタイミングでpassword,password_confitmationにnilをセットしています.
nilをセットしているのは、悪意を持ったユーザーから身を守るためです.(直接データベースからカラムにアクセスされた際の対策)
以上がcreate時にpassword,password_confirmationがnilになり、テストがパスしない原因です.
最後にテストをbuildしたサンプルデータでテストをパスしましょう.

buildしてテスト

テストコード①を無効、②を有効にして、テストを実行すると成功します.
ログイン機構などをgemを利用していると、このようなことがあるので注意しましょう.
またそもそも今回のテストは新規登録が成功するかのテストなので、createしたユーザーでテストをするのはテストの意味が薄れてしまう.(実際のアプリの動作とは異なる)ので書いているテストが、実際のアプリの挙動と同じかどうかを確認しながら書きましょう.