Rails: Fixin' Fixtures with Factory

Chris Wanstrath blogged yesterday about fixin' fixtures with fixture scenarios. He asked for good solutions, so I thought I would write up the solution we're using on my current ThoughtWorks project.

Problems with Fixtures

First, here are my top problems with fixtures:

(1) They're often test specific, yet available for every single test. Using the domain from my entry on Law of Demeter yesterday, a paper boy needs to be able to collect money from a customer. To test the edge cases, we need a customer who doesn't have enough money in his wallet. We could create fixtures for this scenario, but we are unlikely to reuse it much. I would rather see the data creation directly in the test than have to reference fixture files. This ties into my next point:

(2) Fixtures are not easy to maintain. This complaint is voiced the most. Chris made several good points around this in this blog post. I'm not going to go into detail on this, but fixtures are hard to maintain for several reasons. Often they end up incomplete, or missing relationships.

(3) Database state. Using fixtures as implemented in Rails leaves data laying around in the database after each testcase runs. Transactional tests are great, but unfortunately fixtures do not tear down. This means that even though a test case isn't using fixtures :model1, :model2, ..., it may still depend on certain data being in the database that gets left around after another test case loaded certain fixtures.

Introducing the Factory

Enough ranting, onto the solution. We use a factory that has a method for each model named create_. For the domain in point #1, we would have Factory.create_paperboy, Factory.create_customer, and Factory.create_newspaper. These methods are responsible for creating objects in a valid state, with default attributes and associations. Here is how factory methods might be implemented:

module Factory
  def self.create_customer(attributes = {})
    default_attributes = {
      :first_name => "Dan",
      :last_name  => "Manges",
      :paperboy   => create_paperboy
    }
    Customer.create! default_attributes.merge(attributes)
  end

  def self.create_newspaper(attributes = {})
    default_attributes = {
      :customer => create_customer,
      :headline => "Read all about it!"
      :paperboy => create_paperboy
    }
    Newspaper.create! default_attributes.merge(attributes)
  end

  def self.create_paperboy(attributes = {})
    default_attributes = {
      :first_name     => "Paper",
      :last_name      => "Boy",
      :delivery_route => "Main St Route"
    }
    Paperboy.create! default_attributes.merge(attributes)
  end
end

This has worked really well for us. It differs slightly from Object Mother in that Object Mother has specific objects. The example from Martin's post is "Maybe 'John', an employee who just got hired last week; 'Heather' and employee who's been around for a decade." This is similar to having fixtures.

Provides Defaults

The factory provides default values and relationships. A record for any model should be able to be created using Factory.create_[model] without any arguments. The record will have default values and all associations.

Always Valid

Any record returned from the factory should be in a valid state. We use the create! method of ActiveRecord to ensure we cannot get an object in an invalid state.

Improved Readability

Using the factory greatly improves the readability of our tests. This is how a test might look with using fixtures:

def test_paperboy_delivers_to
  assert_equal false, paperboys(:johnny).delivers_to?(customers(:george))
end

This test is difficult to understand. Most of the interesting details about the test are hidden away in yaml files. Here is the same test implemented using the factory:

def test_paperboy_delivers_to
  paperboy = Factory.create_paperboy
  customer = Factory.create_customer
  Factory.create_newspaper :paperboy => paperboy, :customer => customer
  assert_equal true, paperboy.delivers_to?(customer)
end

Everything our test needs is now contained in the test method. This test is both easier to understand and maintain. A failing test will not lead to a rabbit hole down to fixture hell (which often leads to breaking other tests, since developers tend to share fixtures among tests).

Consistent State

If you leave the use_transactional_fixtures configuration option set to true (and you should), Rails will automatically rollback the queries your test ran at the end of the test. (This happens even without specifying fixtures at the top of the test, making the configuration option slightly mis-named). This prevents the problem of having different database state for different tests.

What's Bad

The downside to this approach is that creating a bunch of records for each individual test can be slow. On the project I'm on now, we have sped up our factory by applying default values (I'll blog about that modification later). Also, anytime we're testing using database records, we consider them functional model tests. Our unit tests around models do not hit the database at all and are very fast. This is a good approach when possible. However, sometimes tests need to use the database, and the factory is much better than fixtures for that.

Updates

Paul Gross uses the same approach with a different syntax.

Nathan Herald wrote a plugin, factories_and_workers, to streamline setting up a factory. For more information read his blog entry, the readme, or the source.

Scott Taylor wrote a plugin, FixtureReplacement, that also helps with defining a factory.

Dan Manges Hi, I'm Dan Manges. I'm currently building Root. Previously I was the founding CTO of Braintree. Send me an email.
blog comments powered by Disqus