Why this post?
Have you ever come across a situation where you need to write a test that uses some model objects, but found that those have endless dependencies on the existence of other objects, from the same model or otherwise? Have you ever come across a test where you only care about a specific attribute of a model object, but you find yourself having to populate every single one of the object’s attributes for that test object to be valid? Chances are, you have (or, at least, I hope so, or it might mean you’re not testing at all!). It’s always a pain to have to create 5, 10, 20 objects, and have to specify every single attribute for all those objects, each time you want to test some small new feature or bug fix.
If you ever came across this problem, you may have been lucky enough – after a quick, hungry search landing you on StackOverflow (for example) – to find out about fixtures. Oh, they are amazing things brought to you directly by Rails! They allow you to define and provide some fixed test data for your application, with which your database can be pre-populated before each test. This way, you do not have to constantly create your test data in-place, in an explicit way. You can read more about fixtures in the official Fixtures API documentation.
As you have probably guessed, this makes things a lot easier for us when writing tests. However, fixtures have one important problem: they are too… well, fixed. A single fixture that we define will always create the (pretty much) exact same object. For example, if we create a blog post fixture, it will always have the same title
, content
and published_at
date (for example, two days ago). The amount of flexibility they give us is not very large. What if you want a blog post that has a different published_at
(for example, 1 month from now)? You can create the object from one of the existing fixtures and update the instance after. Luckily for you, this is a simple example. What if you wanted to change the author of the blog post? You would have to create another user and then update the instance. With more complex models, this can easily turn into one of two situations:
- complicated tests to modify fixture-generated stuff, which defeats the purpose of fixtures in the first place.
- an unsustainable number of fixtures to account for all permutations and combinations of attributes that we might need.
So, you decide there has to be something better out in the world, so you hop back onto StackOverflow and discover: factory_bot
!
So, what is factory_bot
?
What is factory_bot
, and how is it different from fixtures? From the official factory_bot repo, it it is defined like this:
factory_bot
is a fixtures replacement with a straightforward definition syntax, support for multiple build strategies (saved instances, unsaved instances, attribute hashes, and stubbed objects), and support for multiple factories for the same class (user
,admin_user
, and so on), including factory inheritance.
factory_bot
allows us to create data factories, which are a sort of blueprint for creating an object (or a set of objects) with predefined attributes. Similarly to fixtures, factories give us quick and easy access to the data we need to run our tests, but with one fundamental difference (among many others): fixtures define fixed objects that populate the database before every test, whereas we can use factories to generate specific objects flexibly whenever we need them for a particular test. They achieve this by having a default state in which they are created, but which can be changed in many ways that we will discuss further ahead.
Another massively useful feature that factory_bot
has is its support for different build strategies. These are a great help in improving test performance and speed in cases where interaction with the database is not strictly required, and we will also discuss them in coming sections. The main idea is that we have the ability to create objects that are not persisted to the database, but stored in memory as objects or as collections of attributes instead.
So, let’s hop right to it!
Basic project to get started
In this post, we will be using a basic todo app as an example, so we can write tests for it and show the power of factories. If you want to learn how this project was set up, you can check out this document. Otherwise, you can directly clone the project and go to the point where all the basic setup was completed:
git clone https://github.com/bfrangi/rails-todo.git cd rails-todo git reset --hard c396394
Feel free to follow along with a project of your own if you prefer!
Let’s write some tests!
Ok, so let’s write some tests for our project. We will start by creating some controller specs for our TasksController
in the spec/controllers/tasks_contorller_spec.rb
file. Just before we do that, we need to install another useful gem that will give us some methods to help us in writing our tests. To do this, add gem ‘rails-controller-testing’
to your Gemfile
, in the development and test group:
# Gemfile group :development, :test do ... gem 'rails-controller-testing' end
And run bundler to install the gem:
bundle install
Also, to be able to perform sign-ins in our controller specs, we need to create a quick helper for our tests:
# spec/support/authentication_spec_helper.rb module AuthenticationSpecHelper def sign_in(user) if user.nil? allow(request.env['warden']).to receive(:authenticate!) .and_throw(:warden, { scope: :user }) allow(controller).to receive(:current_user).and_return(nil) else allow(request.env['warden']).to receive(:authenticate!) .and_return(user) allow(controller).to receive(:current_user).and_return(user) end end end RSpec.configure do |config| config.include AuthenticationSpecHelper, type: :controller end
Awesome! Let’s write a few tests for the index page.
To start off, we can check that when the user is logged in, only their own tasks are shown in the index view. We can also check that the right template is rendered. When the user is not logged in, we can check that they are redirected to the sign in page:
# spec/controllers/tasks_controller_spec.rb require "rails_helper" require "support/authentication_spec_helper" describe TasksController do context "when logged in" do before :each do @logged_in_user = User.create( email: "some.email@some.domain", password: "some_password" ) @other_user = User.create( email: "some.other.email@some.domain", password: "some_other_password" ) sign_in @logged_in_user end describe "GET index" do it "assigns @tasks correctly" do task = @logged_in_user.tasks.create(content: "some content") task_by_other_user = @other_user.tasks.create(content: "some different content") get :index expect(assigns(:tasks)).to eq([task]) end it "renders the index template" do get :index expect(response).to render_template("index") end end end context "when not logged in" do describe "GET index" do it "redirects to the sign_in template" do get :index expect(response).to redirect_to("/users/sign_in") end end end end
And we can run the tests with:
bundle exec rspec -fd ./spec/controllers/tasks_controller_spec.rb
A couple of things to note here:
- First of all, we are requiring the
support/authentication_spec_helper
at the top, which is giving us access to thesign_in
helper to be able to log our user in. assigns
comes from therails-controller-testing
gem we just installed.- We are not using factories yet! We’ve written the test without use of factories so we can see the difference when we do introduce them. Let’s do that in the next section!
How can I create a Factory?
Right, so let’s begin by writing a User factory. In spec/factories/users.rb
, we need to add some stuff to the default User
factory so that the user object is initialised correctly. In this case, we require the email and the password, which we can define like this:
# spec/factories/users.rb FactoryBot.define do factory :user do email { "some.email@some.domain" } password { "some_password" } end end
Cool, let’s modify our existing tests to use the User
factory. We can do that by changing the user definitions in the before block of our controller specs:
# spec/controllers/tasks_controller_spec.rb ... before :each do @logged_in_user = create(:user) @other_user = create(:user) sign_in @logged_in_user end
The create(:user)
statements call the User
factory so that it generates a User
object. However, when we run with the same command from before, we will get a validation error telling us that the email has already been taken. Why does this happen? Well, the way the User
factory is currently defined means that every time we call it, a new user will be created with the email some.email@some.domain
. So when we try to create the second user, there will already be a user with that email address, which will cause the error.
How do we fix this? (drumroll 🥁) I present to you (drumroll getting louder 🥁 🥁) sequences!
What are sequences?
Sequences are a great way to ensure that we comply with uniqueness constraints for factory fields that have them. In our example, there cannot be more than one user with a specific email address. We can ensure this condition is met by using a sequence like this:
# spec/factories/users.rb FactoryBot.define do factory :user do sequence(:email) { |n| "some.email#{n}@some.domain" } password { "some_password" } end end
The way sequences work is that they create a counter for that specific field, which increments with every instance of that factory that we create. In the case of our email example, n
will initially be 1
, so our first user will have an email equal to some.email1@some.domain
. When we create our second user, its email will be some.email2@some.domain
.
Note that, for our second it
block inside the first describe
, we are executing the before
block again, so we are creating two new users different from those used in the first it block. This means that the users will now have the emails some.email3@some.domain
and some.email4@some.domain
.
If we now run our specs again, we will see that the validation error from before is now gone!
Factories with associations
So, we’ve created a User
factory and used it to improve our specs. However, we can still improve them further by creating a Task
factory. In our factories directory, we already have a Task
factory with some default content, but we still need to specify a user. The difference between the user field and the other fields we’ve seen so far is that this is an association. We need an actual user object to be able to populate this. How do we define associations? Like this:
# spec/factories/tasks.rb FactoryBot.define do factory :task do content { "MyText" } association :user, factory: :user end end
There is also a simplification we can make when the factory is named the same as the association:
# spec/factories/tasks.rb FactoryBot.define do factory :task do content { "MyText" } user end end
Every time we create a task using this factory, a new User
object will also be created and associated with the task. This means that we can simplify our tests even further by not defining the second user explicitly:
# spec/controllers/tasks_controller_spec.rb ... before :each do @user = create(:user) sign_in @user end describe "GET index" do it "assigns @tasks correctly" do task = create(:task, user: @user) task_by_other_user = create(:task) get :index expect(assigns(:tasks)).to eq([task]) end ...
Ok, this is great, and our tests are already looking much easier to read. But, there is one more thing about factories which will enable us to take our specs to the next level: nested factories! Let’s have a look at those in the next section.
What are nested factories?
We have a User
factory, which is great. But, most times, we are going to need a user that has some tasks. This is where nested factories come in. A nested factory is a factory that inherits from and extends another factory. We can use this to create a :user_with_tasks
factory that creates a User
object with some tasks:
# spec/factories/users.rb FactoryBot.define do factory :user do sequence(:email) { |n| "some.email#{n}@some.domain" } password { "some_password" } factory :user_with_tasks do transient do tasks_count { 5 } end after(:create) do |user, evaluator| create_list(:task, evaluator.tasks_count, user: user) end end end end
Woah, wait a minute, what do we have here? Let’s break it down:
- First, we have another factory definition within the
:user
factory. Inside that, we have atransient
block. Atransient
block allows us to pass certain parameters to the factory that are not an attribute on the model. This allows us to pass in thetasks_count
parameter, to specify how many tasks we want that user to have. The default is set to5
. Technically, we wouldn’t need to have this block, and we could simply hard-code the number of tasks. But, hey, factories are supposed to be flexible! - Lastly, we have an
after(:create)
callback. This is a block of code that will be executed only after theUser
object has been created. We cannot have a foreign key on the tasks to a user that does not exist. This is why we need to create the user first, and then add the tasks to it.
Nice, so what would our tests look like now?
# spec/controllers/tasks_controller_spec.rb ... before :each do @user = create(:user_with_tasks) @other_user = create(:user_with_tasks) sign_in @user end describe "GET index" do it "assigns @tasks correctly" do get :index expect(assigns(:tasks)).to eq(@user.tasks) end ...
Amazing, this is so much easier to understand and shorter too! Ok, let’s complicate things just a little more.
Let’s complicate things a little more!
Ok, so normally a task can either be completed or not. So far, we have nothing in our app to set that state. To record whether a task is completed or not, we need a new column in our Task
table:
./bin/rails g migration add_completed_to_tasks completed:datetime
This will generate a new migration in our db/migrate
folder, adding a new datetime
column that will be either nil
– when the task is not completed – or it will hold a date and time – the completion date and time for the task. If we open the migrate
folder, we can edit the migration to make the field nullable and give the default of nil
:
# db/migrate/<some_timestamp>_add_completed_to_tasks.rb class AddCompletedToTasks < ActiveRecord::Migration[7.0] def change add_column :tasks, :completed, :datetime, nullable: true, default: nil end end
And we can now migrate:
./bin/rails db:migrate
To enable our users to mark the tasks as completed, we can add add the following in app/views/tasks/_form.html.erb
:
# app/views/tasks/_form.html.erb ... <%= form.text_area :content %> <%= form.label :completed, style: "display: block" %> <%= form.datetime_field :completed %> </div> ...
This gives the following result in the create task form:
And we need to permit the :completed
parameter to be set by the user in the controller:
# app/views/tasks/_form.html.erb ... def task_params params.require(:task).permit(:content, :completed) end ...
We can also add a couple of scopes to our Task
model to make it easier to get all completed/incomplete tasks:
# app/models/task.rb class Task < ApplicationRecord scope :completed, -> { where.not(completed: nil) } scope :not_completed, -> { where(completed: nil) } validates :content, presence: true belongs_to :user end
Ok, so we can now write some specs for this new feature. For example, we could test the scopes to see if they return the correct tasks:
# spec/models/task_spec.rb require 'rails_helper' RSpec.describe Task, type: :model do describe "scopes" do describe ".completed" do it "returns only completed tasks" do completed_task = create(:task, completed: Time.now) not_completed_task = create(:task, completed: nil) expect(Task.completed).to eq([completed_task]) end end describe ".not_completed" do it "returns only not completed tasks" do completed_task = create(:task, completed: Time.now) not_completed_task = create(:task, completed: nil) expect(Task.not_completed).to eq([not_completed_task]) end end end end
And we can run them with the command:
bundle exec rspec -fd ./spec/models/task_spec.rb
Let’s see how traits can help us to simplify this spec!
What are traits?
Traits are extra indications that we can pass into our factory to change the way it behaves. They are useful when we have small-ish characteristics (or set of attribute values) that we want our object to have, which may define a particular state, for example. They avoid us having to create a whole new factory when we only have a small difference with the “basic” factory, avoiding duplication. In our example, we can create a :completed
trait that sets the completed
attribute to true
:
# spec/factories/tasks.rb FactoryBot.define do factory :task do content { "MyText" } user completed { nil } trait :completed do completed { 1.day.ago } end end end
Then our tests become:
# spec/models/task_spec.rb require 'rails_helper' RSpec.describe Task, type: :model do describe "scopes" do describe ".completed" do it "returns only completed tasks" do completed_task = create(:task, :completed) not_completed_task = create(:task) expect(Task.completed).to eq([completed_task]) end end describe ".not_completed" do it "returns only not completed tasks" do completed_task = create(:task, :completed) not_completed_task = create(:task) expect(Task.not_completed).to eq([not_completed_task]) end end end end
Well done, you now master the basics of factory bot! There are so many more things you can do with this amazing library, but these are some of the ones that are most commonly used. In the following section, we will see a short note on build strategies and how you can use them to speed up your tests.
Build strategies
Build strategies are different ways that you can construct objects from a factory. For example, some of the most commonly used strategies are:
create
– This strategy creates an object and saves it to the database. This strategy invokes, in order, theafter_build
,before_create
,to_create
, andafter_create
hooks.build
– This strategy creates an object as you would with.new
, but does not save it to the database. As a result, all database-related things such as IDs are not provided for the built object. This strategy invokes theafter_build
hook.build_stubbed
– This strategy creates a fakeActiveRecord
object. In other words, it is an object that pretends to be persisted to the database (has ID,created_at
,updated_at
, etc.) but is not actually saved to the database at all.
These strategies each have their advantages and disadvantages. For example, the build
and build_stubbed
strategies do not require interaction with the database, which means they are much faster. Tests using these strategies instead of the create strategy will take less time to run. However, sometimes we cannot get away with not persisting objects, for example when we want to test scopes.
One case in which we can use the build
strategy in our tests is to test the completed/not completed state of our tasks. At the moment, we consider any task to be completed if it has a completed date different from nil
. However, it would not make sense to consider a task to be completed if its completed date is in the future. We can add a simple method in our Task
model to determine if the task is really completed or not (we also need to update our scopes):
# app/models/task.rb class Task < ApplicationRecord scope :completed, lambda { where('completed <= ?', Time.now) } scope :not_completed, lambda { where('completed IS NULL OR completed > ?', Time.now) } validates :content, presence: true belongs_to :user def completed? !completed.nil? && completed <= Time.now end end
We could now update the specs for the scopes, but we will focus on adding new specs for the completed?
method so we can see some of the build
strategies in action:
# spec/models/task_spec.rb ... describe "methods" do describe ".completed?" do it "returns false when completed is set to nil" do not_completed_task = build(:task) expect(not_completed_task.completed?).to eq(false) end it "returns false when completed is a date in the future" do not_completed_task = build(:task, completed: 1.day.from_now) expect(not_completed_task.completed?).to eq(false) end it "returns true when completed is a date in the past" do completed_task = build(:task, :completed) expect(completed_task.completed?).to eq(true) end end end end
If we now run and time this block of specs 10 times with the build
strategy and another 10 times with the create
strategy:
for i in {1..10}; do bundle exec rspec -fd ./spec/models/task_spec.rb:24; done
We see that the average time taken with build
is noticeably smaller than that taken with create
(take into account that these times include loading the file). With more complex tests, imagine how important that may be! In my case:
- Average time taken with create: 2164 ms
- Average time taken with build: 2030 ms
That’s a difference of 0.134 seconds for just three simple tests!
Note: if you want to try timing the commands yourself, this might be useful for that.
Conclusion
So, in this post you’ve learnt how to use factories to make your specs shorter, more concise, and more readable, in a way in which the things (model attributes, states, etc.) that are important to the specific test case are made obvious by the code itself, whereas everything that is necessary but not relevant to the specific test case is done behind the scenes by factory_bot
. You’ve also learnt to use factories to speed up your tests by not having to persist model objects to the database if that is not needed. As a bonus, you’ve now started a cool tasks project that will help you keep much more organised in the future! Feel free to extend it and improve it in any way you can think (and don’t forget to keep up the coverage 😁). In case you want the complete project, check out the GitHub repository here.
References
Creating a simple to do app using rails
Adding associations to models (StackOverflow)
Authenticating pages (GoRails)
Getting started with factory_bot (Thoughtbot GitHub)
Simulating a login with Rspec (StackOverflow)
Getting Sequential: A look into a Factory Bot Anti-pattern (Thoughtbot)