DEV Community

loading...

The optimal way to create a set of records with FactoryBot.create_list & FactoryBot.build_list

Hernan Velasquez
Ruby on Rails, Javascript frameworks and general web development
・3 min read

The purpose

Recently I advised a fellow Rubyist trying to create 20 records in rspec to change something like:

# arbitrarily, assign even numbers to employee_number in this example
let(:first) { create (:employee, employee_number: 2) }
let(:second) { create (:employee, employee_number: 4) }
let(:third) { create (:employee, employee_number: 6) }
let(:fourth) { create (:employee, employee_number: 8) }
let(:fifth) { create (:employee, employee_number: 10) }
...
let(:tenth) { create (:employee, employee_number: 20) }
Enter fullscreen mode Exit fullscreen mode

To something like:

let(:employees) { create_list(:employee, 20) } 
Enter fullscreen mode Exit fullscreen mode

But unfortunately this wasn't that simple, so I want to share this quick write up to expose the problems we faced and the final solution we reached.

The uniqueness validation

For the sake of this example, let's build our employee model as:

class Employee
   validates_uniqueness_of :employee_number
end
Enter fullscreen mode Exit fullscreen mode

And the migration:

   add_column :employees, :employee_number, :integer, default: 0
Enter fullscreen mode Exit fullscreen mode

So any attempt to call create_list will result in an exception Validation failed: employee_number should be unique since FactoryBot will try to create the 20 records with this field in 0. Even trying:

let(:employees) { create_list(:employee, 20, employee_number: 20) } 
Enter fullscreen mode Exit fullscreen mode

Will also fail since FactoryBot will try to create the 20 records with this field in 20.

First approach, the block

Reading FactoryBot documentation, we found that you can pass a block to create_list to manipulate the record to be created, so our next approach was doing:

let(:employees) { 
  create_list(:employee, 20) do |record, i|
    # arbitrarily, assign even numbers to employee_number in this example
    record.employee_number = i * 2
  end
} 
Enter fullscreen mode Exit fullscreen mode

First approach failed, why?

Unfortunately FactoryBot folks haven't documented the use of the block properly. When we read the docs the first time, we believed that create_list was built as something like

def create_list
  object = build the object
  yield(object) if block_given?
  object.save!
end
Enter fullscreen mode Exit fullscreen mode

But the way they built this was something like:

def create_list
  object = build the object
  object.save!
  yield(object) if block_given?
end
Enter fullscreen mode Exit fullscreen mode

So what we see here is the same effect of:

let(:employees) { create_list(:employee, 20) } 
Enter fullscreen mode Exit fullscreen mode

Trying to create 20 employees with employee_number set to its default value, 0, basically because he is saving the object before giving us the opportunity to manipulate the employee_number as we see fit.

Second approach, saving in the block.. will it work?

So yeah... next think we thought of was doing:

let(:employees) { 
  create_list(:employee, 20) do |record, i|
    # arbitrarily, assign even numbers to employee_number in this example
    record.employee_number = i * 2
    record.save!
  end
} 
Enter fullscreen mode Exit fullscreen mode

But, this will work? The answer ..... NOPE... but, why?

On the first iteration, it will create an employee with employee_number 0 * 2 = 0

On the second iteration, expect FactoryBot to create an employee with employee_number 1 * 2 = 2, but as he creates the record before yielding, then it first creates the record with its default (guess what, 0), so yeap, we'll get Validation failed: employee_number should be unique again.

Even if we don't have the uniqueness validation, it seems weird to create a persisted object (insert) to immediately update it (update).

Third and final approach, using build_list instead.

So, how is the best way to create a set of records using FactoryBot when you have weird validations to take care of?

let(:employees) { 
  build_list(:employee, 20) do |record, i|
    # arbitrarily, assign even numbers to employee_number in this example
    record.employee_number = i * 2
    record.save!
  end
} 
Enter fullscreen mode Exit fullscreen mode

build_list was intended to just build the object in memory without persisting them, so we can use it in combination to save! within a block to create the list we wanted without triggering the validations before saving them.

Its better to use save! instead of save so you will see any other validation error in case it happens to you.

Happy testing!

Discussion (0)