FixtureBuilder

Based on the code from fixture_scenarios, by Chris Wanstrath. Allows you to build file fixtures from existing object mother factories, like FactoryGirl, to generate high performance fixtures that can be shared across all your tests and development environment.

The best of all worlds!

Installing

  1. Install:

  2. Directly: gem install fixture_builder

  3. Bundler:

    "`ruby # Gemfile group :development, :test do gem 'fixture_builder'

    “`

  4. Create a file which configures and declares your fixtures (see below for examples)

  5. Require the above file in your spec_helper.rb or test_helper.rb

  6. If you are using rspec, ensure you have

    • Set the FIXTURES_PATH in config/application.rb (not test.rb, or you can't use rake db:fixtures:load). E.g.:

    ruby module MyApp class Application < Rails::Application #... ENV['FIXTURES_PATH'] = 'spec/fixtures' #... * Set config.fixture_path = Rails.root.join('spec/fixtures') in spec/rails_helper.rb * Set config.global_fixtures = :all if you don't want to specify fixtures in every spec file.

  7. You probably also want to use {config.use_transactional_fixtures} (if you are using rspec) or {use_transactional_fixtures/use_transactional_tests} (if you are not using rspec),

  8. If you are using fixtures in Selenium-based Capybara/Cucumber specs that runs the tests and server in separate processes, you probably want to consider setting transactional fixtures to false, and instead using Database Cleaner with DatabaseCleaner.strategy = :truncation or DatabaseCleaner.strategy = :deletion.

Usage

Configuration Example

spec/rails_helper.rb or test/test_helper.rb:

require_relative 'support/fixture_builder'

When using an object mother such as factory_girl it can be setup like the following:

# spec/support/fixture_builder.rb
FixtureBuilder.configure do |fbuilder|
  # rebuild fixtures automatically when these files change:
  fbuilder.files_to_check += Dir["spec/factories/*.rb", "spec/support/fixture_builder.rb"]

  # now declare objects
  fbuilder.factory do
    david = Factory(:user, :unique_name => "david")
    ipod = Factory(:product, :name => "iPod")
    Factory(:purchase, :user => david, :product => ipod)
  end
end

The block passed to the factory method initiates the creation of the fixture files. Before yielding to the block, FixtureBuilder cleans out the test database completely. When the block finishes, it dumps the state of the database into fixtures, like this:

# users.yml
david:
  created_at: 2010-09-18 17:21:23.926511 Z
  unique_name: david
  id: 1

# products.yml
i_pod:
  name: iPod
  id: 1

# purchases.yml
purchase_001:
  product_id: 1
  user_id: 1

FixtureBuilder guesses about how to name fixtures based on a prioritized list of attribute names. You can also hint at a name or manually name an object. Both of the following lines would work to rename purchase_001 to davids_ipod:

fbuilder.name(:davids_ipod, Factory(:purchase, :user => david, :product => ipod))
@davids_ipod = Factory(:purchase, :user => david, :product => ipod)

Another way to name fixtures is to use the name_model_with. To use it you create a block that returns how you want a certain model name based on the record field.

fbuilder.name_model_with(User) do |record|
  [record['first_name'], record['last_name']].join('_')
end

For all User fixture {first_name: 'foo', last_name: 'bar'} it would generate foo_bar as the fixture name.

There are also additional configuration options that can be changed to override the defaults:

By default these are set as:

Sequence Collisions

One problem with generating your fixtures is that sequences can collide. When the fixtures are generated only as needed, sometimes the process that generates the fixtures will be different than the process that runs the tests. This results in collisions when you still use factories in your tests.

There's a couple of solutions for this.

Here's a solution for FactoryGirl which resets sequences numbers to 1000 (to avoid conflicts with fixture data which should be sequenced < 1000) before the tests run:

FixtureBuilder.configure do |fbuilder|
  # ...
end

# Have factory girl generate non-colliding sequences starting at 1000 for data created after the fixtures

# Factory Girl <2 yields name & seq
# Factory Girl >2 yeilds only seq
FactoryGirl.sequences.each do |seq|
 
  # Factory Girl 4 uses an Enumerator Adapter, otherwise simply set a Fixnum
  seq.instance_variable_set(:@value, FactoryGirl::Sequence::EnumeratorAdapter.new(1000))
  
end

Another solution is to explicitly reset the database primary key sequence via ActiveRecord. You could call this method before you run your factories in the fixture_builder.rb block:

def reset_pk_sequences
  puts 'Resetting Primary Key sequences'
  ActiveRecord::Base.connection.tables.each do |t|
    ActiveRecord::Base.connection.reset_pk_sequence!(t)
  end
end

It's probably a good idea to use both of these approaches together, especially if you are going to fall back to using FactoryGirl object mothers in addition to fixtures.

Tips

More Complete Config Example

As you get more fixtures, you may want to move the creation of fixtures to a separate file. For example:

# spec/support/fixture_builder.rb
require_relative 'create_fixtures'

FixtureBuilder.configure do |fbuilder|
  # rebuild fixtures automatically when these files change:
  fbuilder.files_to_check += Dir[
    "spec/factories/*.rb",
    "spec/support/fixture_builder.rb",
    "spec/support/create_fixtures.rb",
  ]

  # now declare objects
  fbuilder.factory do
    CreateFixtures.new(fbuilder).create_all
  end
end

# Have factory girl generate non-colliding sequences starting at 1000 for data created after the fixtures
FactoryGirl.sequences.each do |seq|
  seq.instance_variable_set(:@value, FactoryGirl::Sequence::EnumeratorAdapter.new(1000))
end

Then, you can do more extensive and advanced fixture creation in that class. Here's a partial example:

# spec/support/create_fixtures.rb

require 'factory_girl_rails'

class CreateFixtures
  include FactoryGirl::Syntax::Methods

  attr_accessor :fbuilder, :models, :fixed_time

  def initialize(fbuilder)
    @fbuilder = fbuilder
    @models = {}
    @fixed_time = Time.utc(2015, 3, 14, 9, 2, 6)
  end

  def create_all
    reset_pk_sequences
    create_users
    create_products
    create_purchases
    reset_pk_sequences
  end

  private

  def reset_pk_sequences
    puts 'Resetting Primary Key sequences'
    ActiveRecord::Base.connection.tables.each do |t|
      ActiveRecord::Base.connection.reset_pk_sequence!(t)
    end
  end
  
  def create_users
    # etc...
  end 
  
  # other creation and helper methods to abstract common logic, e.g.
  # * custom naming rules via #name_model_with
  # * set up associations by storing created model records in a hash so you can retrieve them
  # etc... (hopefully some of these helper patterns can be standardized and included in the gem in the future)
 end

Copyright © 2009 Ryan Dy & David Stevenson, released under the MIT license

Currently maintained by Chad Woolley