Rails Testing Antipatterns: Fixtures and Factories

· 14 Jan 2014 · Semaphore Engineering Blog

In the upcoming series of posts, we'll explore some common antipatterns in writing tests for Rails applications. The presented opinions come from our experience in building web applications with Rails (we've been doing it since 2007) and is biased towards using RSpec and Cucumber. Developers working with other technologies will probably benefit from reading as well.

Antipattern zero: no tests at all

If your app has at least some tests, congratulations: you're among the better developers out there. If you think that writing tests is hard — it is, but you just need a little more practice. I recommend reading the RSpec book if you haven't yet. If you don't know how to add more tests to a large system you inherited, I recommend going through Working Effectively with Legacy Code. If you have no one else to talk to about testing in your company, there are many great people to meet at events such as CITCON.

If you recognize some of the practices discussed here in your own code, don't worry. The methodology is evolving and many of us have "been there and done that". And finally, this is all just advice. If you disagree, feel free to share your thoughts in the comment section below. Now, onwards with the code.

Fixtures and factories

Using fixtures

Fixtures are Rails' default way to prepare and reuse test data. Do not use fixtures.

Let's take a look at a simple fixture:

# users.yml
marko:
  first_name: Marko
  last_name: Anastasov
  phone: 555-123-6788

You can use it in a test like this:

describe User do
  describe "#full_name" do
    it "is composed of first and last name" do
      user = users(:marko)
      user.full_name.should eql("Marko Anastasov")
    end
  end
end

There are a few problems with this test code:

  • It is not clear where the user came from and how it is set up.
  • We are testing against a "magic value" — implying something was defined in some code, somewhere else.

In practice these shortcomings are addressed by comment essays:

describe Dashboard do

  fixtures :all

  describe "#show" do
    before do
      # User with preferences to view posts about kittens
      # and in the group with special access to Burmese cats
      # with 4 friends that like ridgeback dogs.
      @user = users(:kitten_fan)
    end
  end
end

Maintaining fixtures of more complex records can be tedious. I recall working on an app where there was a record with dozens of attributes. Whenever a column would be added or changed in the schema, all fixtures needed to be changed by hand. Of course I only recalled this after a few test failures.

A common solution is to use factories. If you recall from the common design patterns, factories are responsible for creating whatever you need to create, in this case records. Factory Girl is a good choice.

Factories let you maintain simple definitions in a single place, but manage all data related to the current test in the test itself when you need to. For example:

FactoryGirl.define do
  factory :user do
    first_name "Marko"
    last_name  "Anastasov"
    phone "555-123-6788"
  end
end

Now your test can set the related attributes before checking for the expected outcome:

describe User do
  describe "#full_name" do
    before do
      @user = build(:user, :first_name => "Johnny", :last_name => "Bravo")
    end

    it "is composed of first and last name" do
      @user.full_name.should eql("Johnny Bravo")
    end
  end
end

A good factory library will let you not just create records, but easily generate unsaved model instances, stubbed models, attribute hashes, define types of records and more — all from a single definition source. Factory Girl's getting started guide has more examples.

Factories pulling too many dependencies

Factories let you specify associations, which get automatically created. For example, this is how we say that creating a new Comment should automatically create a Post that it belongs to:

FactoryGirl.define do
  factory :comment do
    post
    body "groundbreaking insight"
  end
end

Ask yourself if creating or instantiating that post in every call to the Comment factory is really necessary. It might be if your tests require a record that was saved in the database, and you have a validation on Comment#post_id. But that may not be the case with all associations.

In a large system, calling one factory may silently create many associated records, which accumulates to make the whole test suite slow (more on that later). As a guideline, always try to create the smallest amount of data needed to make the test work.

Factories that contain unnecessary data

A spec is effectively a specification of behavior. That is how we look at it when we open one. Similarly, we look at factories as definitions of data necessary for a model to function.

In the first factory example above, including phone in User factory was not necessary, if there is not a validation of presence. If the data is not critical, just remove it.

Factories depending on database records

Adding a hard dependency on specific database records in factory definitions leads to build failures in CI environment. Consider the following example:

factory :active_schedule do 
  start_date Date.today - 1.month 
  end_date 1.month.since(Date.today) 
  processing_status 'processed' 
  schedule_duration ScheduleDuration.find_by_name('Custom') 
end

It is important to know that the code for factories is executed when the Rails test environment loads. This may not be a problem locally because the test database had been created and some kind of seed structure applied some time in the past. In the CI environment however the builds starts from a blank database, so the first Rake task it runs will fail. To reproduce and identify such issue locally, you can do db:drop, followed by db:setup.

One way to fix this is to use factory_girl's traits:

factory :schedule_duration do
  name "Test Duration"

  trait :custom do
    name "Custom"
  end
end

factory :active_schedule do
  association :schedule_duration, :custom
end

Another is to defer the initialization in a callback. This however adds an implicit requirement that test code defines the associated record before the parent:

factory :active_schedule do
  before(:create) do |schedule|
    schedule.schedule_duration = ScheduleDuration.find_by_name('Custom')
  end
end

Got anything to add? I'd love to hear your comments below.

Update 2014-01-22: Changed fixtures examples to use key-based lookup.

comments powered by Disqus
Newsletter

Occasional lightweight product and blog updates. Unsubscribe at any time.

© 2009-2017 Rendered Text. All rights reserved. Terms of Service, Privacy policy, Security.