Factories: don’t stop production!

Posted by on August 29, 2023

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 the sign_in helper to be able to log our user in.
  • assigns comes from the rails-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 a transient block. A transient block allows us to pass certain parameters to the factory that are not an attribute on the model. This allows us to pass in the tasks_count parameter, to specify how many tasks we want that user to have. The default is set to 5. 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 the User 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, the after_build, before_create, to_create, and after_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 the after_build hook.
  • build_stubbed – This strategy creates a fake ActiveRecord 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)

Transient blocks in factory_bot factories (StackOverflow)

Build strategies (Thoughtbot)

Leave a reply

Your email address will not be published. Required fields are marked *