factory_data_preloader

If you’re like me, you really dislike using rails test fixtures. On the rails projects I’ve worked on that have used test fixtures, we’ve had a number of tests that passed as false positives because the test fixtures don’t represent “real data”. Fixtures just dump the data you’ve defined directly into the database, without going through the model–meaning you don’t get any of the validations or before/after save callback behavior defined on your models.

There are multiple gems and plugins that address this; my personal favorite is factory girl.

However, none of the solutions I’ve tried are as fast as using test fixtures. When you use test fixtures, rails rolls back the database transaction used in each test and allows you to re-use your fixture data.

This is much much quicker then creating the data you will use directly in each test.

This gem attempts to solve this issue, and give you the best of both worlds: create your test data using factory girl, the models themselves, or any other solution you want, while also being able to pre-load it and re-use it in each test when rails rolls back the transaction.

Download

Github: github.com/myronmarston/factory_data_preloader

Gem:

gem install factory_data_preloader --source http://gemcutter.org

Usage

Load the gem using Rails’ 2.1+ gem support, in either config/environment.rb, or config/environments/test.rb:

config.gem 'factory_data_preloader', :source => 'http://gemcutter.org'

Define your preloaded data. FactoryData will automatically require test/factory_data.rb or test/factory_data/*.rb. Define your data in these files like this:

FactoryData.preload(:users) do |data|
  data.add(:thom) { User.create(:first_name => 'Thom', :last_name => 'York') }
  data.add(:john) { User.create(:first_name => 'John', :last_name => 'Doe') }
end

FactoryData.preload(:posts, :depends_on => :users) do |data|
  # note the use of the :depends_on option to force the users to be loaded first.
  data.add(:tour) { FactoryData.users(:thom).posts.create(:title => 'Tour!', :body => 'Radiohead will tour soon.') }
end

FactoryData.preload(:some_other_posts, :model_class => Post, :depends_on => :users) do |data|
  # note the use of the :model_class option when the model class cannot be inferred from the symbol.
  data.add(:another_post) { Post.create(:user => FactoryData.users(:john), :title => 'Life is good') }
end

FactoryData.preload(:comments, :depends_on => [:users, :posts]) do |data|
  # :depends_on lets you pass an array
  data.add(:woohoo) { FactoryData.users(:john).comments.create(:post => FactoryData.posts(:tour), :comment => "I can't wait!") }
end

Finally, use this preloaded data in your tests:

# test/user_test.rb
class UserTest < ActiveSupport::TestCase
  def test_thom_has_last_name
    user = FactoryData.users(:thom)
    assert_equal 'York', user.last_name
  end
end

# test/post_test.rb
class PostTest < ActiveSupport::TestCase
  def test_post_has_body
    post = FactoryData.posts(:tour)
    assert_not_nil post.body
  end
end

All factory data is automatically preloaded for all tests. In a large rails application, this can incur a significant performance penalty at the start of each test run. If you want finer grain control over which preloaders run, you can configure it like so:

# in test_helper.rb
FactoryDataPreloader.preload_all = false

# test/user_test.rb
class UserTest < ActiveSupport::TestCase
  preload_factory_data :users # multiple types can be listed as necessary
  # tests go here...
end

# test/post_test.rb
class PostTest < ActiveSupport::TestCase
  preload_factory_data :posts # dependencies are taken into account, so users will automatically be preloaded as well.
  # tests go here...
end

Notes, etc.

  • This gem has been tested with Rails 2.2.2, 2.3.2, 2.3.3 and 2.3.4.

  • You can create the data using any fixture replacement you want. In this contrived example, I just used ActiveRecord’s built in methods for simplicity’s sake.

  • FactoryData#preload does not actually preload the data. It simply defines the data that will be automatically preloaded at the appropriate time (namely, at the same time when rails loads the fixtures).

  • FactoryData#preload defines a new method on FactoryData using the same name as the symbol.

  • This can be mixed-n-matched with fixtures. You may want to keep using fixtures in an existing code base, or migrate over to this slowly.

  • If you have dependencies between your preloaded data, you can use the :depends_on option to force some records to be preloaded before others. Where no dependencies exist, the preloaders are run in the order they are defined.

  • FactoryData#preload attempts to infer the appropriate model class from the symbol you pass. If your symbol doesn’t match the model class, pass the model class using the :model_class option.

  • The preloader will also delete all records from the database, before any preloading begins. This is done at the same time that rails deletes records for test fixtures. The tables are cleared using the reverse of the order defined by your :depends_on options, so be sure to use :depends_on if you have foreign key constraints.

  • Errors that occur during preloading are handled smartly: a warning is printed when one occurs, and an exception is raised when the record that had the error is accessed. This should allow the rest of the data to be preloaded, and cause only the tests that access the records with errors to fail.

  • The syntax used before the 0.5.0 release is still allowed but has been deprecated. Instead of this:

    FactoryData.preload(:users) do |data|
      data[:thom] = User.create(:first_name => 'Thom', :last_name => 'York')
      data[:john] = User.create(:first_name => 'John', :last_name => 'Doe')
    end
    

Use this:

FactoryData.preload(:users) do |data|
  data.add(:thom) { User.create(:first_name => 'Thom', :last_name => 'York') }
  data.add(:john) { User.create(:first_name => 'John', :last_name => 'Doe') }
end

Known Issues

  • This gem doesn’t play nice with autotest.

Copyright © 2009 Myron Marston, Kashless.org. See LICENSE for details.