FactoryBot - thuy-econsys/rails_app GitHub Wiki

Setup

In the Gemfile:

group :development, :test do
  gem "factory_bot_rails"
end

Include FactoryBot::Syntax::Methods to facilitate the simpler invocation of FactoryBot methods without having to prepend FactoryBot. For example, create() instead of FactoryBot.create(). Either in /spec/rails_helper or a dedicated /spec/support/factory_bot.rb file, within the RSpec.configure block:

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

With a separate setup /spec/support/factory_bot.rb, there needs to be a require in the /spec/rails_helper.rb:

require 'support/factory_bot'

Define factories

Make sure to define factories either in factory file /spec/factories.rb or in a factories directory /spec/factories/*.rb.

FactoryBot.define do
  factory :user do
    email { "[email protected]" }
    password { "password" }
  end
end

Invoking create(:user) creates a User instance that can be tested against. Adding attributes as options overrides the original attributes defined in the factory: create(:user, password: 'notthatone').

To see what attributes an object has, use the attributes_for method by passing the object as an argument. It will return a hash of the attributes, so it's possible to access the value of a specific attribute with square brackets:

attributes_for(:user)         # { :email => "[email protected]", :password => "notthatone" } 
attributes_for(:user)[:email] # "[email protected]"

Use attributes_for with let to generate a hash of valid attributes for your specs:

let(:valid_attributes) { attributes_for(:user) }

...
    context "for POST #create" do
      it "creates a new user" do
        expect {
          post :create, params: {
            :user => valid_attributes
          }
        }.to change{ User.count }.by(1)
      end
      it "redirects to user path, show action" do
        post :create, params: {
          :user => valid_attributes
        }
        expect(response).to have_http_status(302)
        expect(response).to redirect_to(user_url(assigns(:user)))
        expect(response).to redirect_to(:action => :show, :id => assigns(:user).id)
      end
    end
...

Instead of an instance variable use the lazy let to generate a user object for testing as it will take up less resources:

let(:test_user) { create(:user) }

...
    context "for GET #edit" do
      it "returns a 200 OK status" do
        get :edit, params: {
          :id => test_user.to_param
        }
        expect(response).to have_http_status(200)
        expect(response).to have_http_status(:ok)
      end
      it "renders an 'edit' template" do
        get :edit, params: {
          :id => test_user.to_param
        }
        expect(response).to render_template(:edit)
      end
    end
...

setup different users using FactoryBot trait

One place to override attributes but also maintain the original is within the factory utilizing trait.

  factory :user do
    email { "[email protected]" }
    password {"Password1!"}
    password_confirmation {"#{password}"} 
    account_type {0}
    
    # attributes for child user overriding parent traits
    trait :admin do
      email { "[email protected]" }
      password {"Password2!"}
      account_type {1}
    end
    trait :moderator do
      email { "[email protected]" }
      password {"Password3!"}
      account_type {2}
    end

    # register child factories with their overriding traits
    factory :admin, traits: [:admin] 
    factory :moderator, traits: [:moderator]
  end
end

build vs create

FactoryBot's build() is instantiating an object, similar to Ruby's .new. create() instantiates and saves the object to the database, similar to .new followed by a .save which is essentially a .create. Keep this in mind if you ever get rspec error:

ActiveRecord::RecordInvalid:
        Validation failed: Email has already been taken

The User model has uniqueness validation for the email attribute. If the value is hard-coded in the factory file, the tests will not even run with the invocation of create(:user). One way to get around the error is to use sequence to increment the FactoryBot object where n is the integer incrementing. Another is to utilize Faker's unique method.

# /spec/factories.rb

FactoryBot.define do
  factory :user do
    name { Faker::Name.unique.name }
    sequence(:email) { |n| "#{name.parameterize}-#{n}@example.com" } # [email protected], [email protected], [email protected]...
  end
end

The following resets the sequence between each and every test. Not sure if it's needed as running examples as transactions starts each example with a clean database:

# ./spec/rails_helper.rb

RSpec.configure do |config|
  config.after do
    FactoryBot.rewind_sequences
  end
end

References