Stepford

FactoryGirl = so easy now

Stepford is an automatic required (non-null or presence validated) association resolver and factory generator for FactoryGirl.

The Stepford CLI allows can generate a factories.rb or multiple files each defining a single factory, for every existing model or for those specified.

The following would create/overwrite test/factories.rb with a factory for every model in app/models:

bundle exec stepford factories

If you use rspec, it would be:

bundle exec stepford factories --path spec

With our rspec helper, you can use this to create a bar and automatically create its dependencies and their dependencies, etc. providing ways to remedy circular dependencies:

deep_create(:bar)

You can also create_list, build, build_list, and build_stubbed:

deep_build_list(:bar, 5)

Need to customize it? You can use the normal FactoryGirl behavior (args, options, block), but in addition, you may specify options for each factories that would create direct or indirect associations.

e.g. maybe Bar has a required association called house_special which uses the beer factory, and we have a block we want to send into it, and Beer has specials that you want to build as a list of 3, using the tuesday_special_offer factory. In rspec, you'd do:

deep_create_list(:bar, with_factory_options: {
  house_special: [:create, :beer, {blk: ->(beer) do; beer.bubbles.create(attributes_for(:bubbles)); end}],
  specials: [:build_list, :tuesday_special_offer, 3]
}) do
  # any block you would send to FactoryGirl.create_list(:bar) would go here
end

By default autogenerated factories just have required attributes, e.g.:

require 'factory_girl_rails'

FactoryGirl.define do

  factory :novel do
    created_at { 2.weeks.ago }
    name 'Test Name'
    price 1.23
    sequence(:isbn)
    sequence(:ean)
    updated_at { 2.weeks.ago }
  end

end

But you can have it autogenerate all attributes, associations, and traits:

require 'factory_girl_rails'

FactoryGirl.define do

  factory :novel do
    author
    association :edited_by, factory: :user
    FactoryGirl.create_list :comments, 2
    trait :with_notes do; FactoryGirl.create_list :note, 2; end
    trait :complete do; complete true; end
    trait :not_complete do; complete false; end
    created_at { 2.weeks.ago }
    name 'Test Name'
    price 1.23
    sequence(:isbn)
    sequence(:ean)
    description 'Test Description'
    trait :with_summary do; template 'Test Summary'; end
    updated_at { 2.weeks.ago }
  end

end

However, without modification, if you use the CLI to generate associations, you may run into association interdependency problems (circular references). To fix those you could hand-edit the factories, or write methods to create what is needed. Or, to keep it simple, just use the defaults for the factories CLI and then use the deep_* methods in your specs to automatically create dependencies as needed!

Setup

In your Rails 3+ project, add this to your Gemfile:

gem 'stepford'

If you don't already have it, add this also:

gem 'factory_girl_rails'

Then run:

bundle install

Configuration

You don't have to use a config/stepford.rb, but if you have one, it will load it as needed both in CLI and via helpers, etc.

If you don't use the CLI, you can just put it into your test/test_helper.rb, spec/spec_helper.rb, or similar for the deep_* methods, and with the CLI you could just put it into a Rails environment file.

Debug option:

Stepford::FactoryGirl.debug = true

Make Stepford think that the schema looks different than it does to allow virtual attributes, etc.:

Stepford::FactoryGirl.column_overrides = {
  [:bartender, :experience] => {null: false},
  [:patron, :time_entered_bar] => {null: false},
  [:patron, :some_virtual_attribute] => {null: false, virtual: true, type: :string, limit: 5} # if you specify a virtual attribute, be sure to include virtual: true and a valid type
}

Override options are: :virtual, :type, :limit, :default, :null, :precision, and :scale. Each is equivalent to their ActiveRecord column equivalents.

You can reconfigure it at runtime during tests if you'd like, and you can just call this if you want, but it doesn't have to be loaded this way. No magic involved, but it caches a little, so faster than just doing a load:

Stepford::FactoryGirl.load_config(pathname)

Usage

Require

Put this in your test/spec_helper.rb, spec/spec_helper.rb, or some other file used by your tests:

require 'stepford/factory_girl'

Stepford::FactoryGirl

Stepford::FactoryGirl acts just like FactoryGirl, but it goes through all the null=false associations for foreign keys that aren't primary keys in the factory and/or its presence validated associations and attempts to create required association data. Pass in the option :with_factory_options with a hash of factory name symbols to the arguments and block you'd pass to it if you want to change only parts of required dependent associations that are created. You specify the block using a :blk option with a lambda.

If you don't specify options, it's easy (note: it is even easier with the rspec helper- see below). If Foo requires Bar and Bar requires a list of Foobars and a Barfoo, and you have factories for each of those, you'd only have to do:

Stepford::FactoryGirl.create_list(:foo, 5)

and that would create a list of 5 Foos, that each have a Bar, where each Bar has a list of 2 Foobars and a Barfoo. Easy!

But, you might want to specify traits, and certain attributes or associations or a block or different methods to use. That's pretty easy, too. Let's say you only need to tweak bar and foobar on each item, but the rest gets created as it would with just Stepford::FactoryGirl.create_list, so if you wanted to create 5 with two traits :fancy and :light and only build the bar and build bar's foobar as a stub:

Stepford::FactoryGirl.create_list(:foo, 5, :fancy, :light, with_factory_options: {
  bar: [:create_list, :bar],
  foobar: [:create, :foobar, :some_trait, :some_other_trait, blk: -> {block you would send to foobar.create goes here}]
}) do
  # any block you would send to FactoryGirl.create_list(:foo) would go here
end
RSpec Helpers

Put this in your spec/spec_helper.rb:

require 'stepford/factory_girl/rspec_helpers'

Then you can just use deep_create, deep_create_list, deep_build, deep_build_list, or deep_build_stubbed in your rspec tests (deep_create becomes a shortcut for ::Stepford::FactoryGirl.create, etc.), e.g.:

deep_create(:foo)
Cleaning Up

If you just want to run rspec at command-line, want to be able to create in before hooks, and don't want to mess with database cleaner, here is some code that you can add to your spec_helper to remove all model instances.

THIS WILL DELETE ALL YOUR DATA! BE EXTREMELY CAREFUL:

raise "Do you really want to delete all #{Rails.env} data? I think not." unless Rails.env == 'test'

# ActiveRecord::Base.subclasses doesn't get everything
ALL_MODEL_CLASSES = Dir[File.join('app','models','*.rb').to_s].collect do |filename|
  model_name = File.basename(filename).sub(/.rb$/, '')
  load File.join('app','models',"#{model_name}.rb")
  begin
    model_class = model_name.camelize.constantize
  rescue => e
    puts "Problem in #{model_name.camelize}"
    raise e
  end
  next unless model_class.ancestors.include?(ActiveRecord::Base)
  model_class
end.compact

# can run rspec instead of rake test. FactoryGirl doesn't clean up everything, and DatabaseCleaner is either too slow (truncation) or too transaction-y (transaction).
RSpec::Runner.configure do |config|
  config.before(:suite) do
    ALL_MODEL_CLASSES.each do |m|
      begin
        m.delete_all
      rescue
      end
    end
    ALL_MODEL_CLASSES.each do |m|
      count = m.count
      raise "#{m} not all deleted (found #{count})" if count > 0
    end
  end

  config.after(:all) do
    ALL_MODEL_CLASSES.each do |m|
      begin
        m.delete_all
      rescue
      end
    end
    ALL_MODEL_CLASSES.each do |m|
      count = m.count
      raise "#{m} not all deleted (found #{count})" if count > 0
    end
  end
end
Debugging

Add somewhere after the require:

Stepford::FactoryGirl.debug = true

CLI

Factories
Creating Factories

To autogenerate test/factories.rb from all model files in app/models:

bundle exec stepford factories

If you want one file per model, specify --multiple. Use --path to specify the directory path or factories.rb pathname. The default path is test/factories, which it assumes exists. In that directory, it will create a factory file for each model. If you want separate factory files in spec/factories, you'd use:

bundle exec stepford factories --path spec/factories --multiple
RSpec

To put all of your factories into spec/factories.rb:

bundle exec stepford factories --path spec

This also works:

bundle exec stepford factories --path spec/support/factories.rb
Specifying Models

By default, Stepford processes all models found in app/models.

Specify --models and a comma-delimited list of models to only output the models you specify. If you don't want to overwrite existing factory files, you should direct the output to another file and manually copy each in:

bundle exec stepford factories --path spec/support/put_into_factories.rb --models foo,bar,foo_bar
Associations

If you use Stepford::FactoryGirl (or deep_* methods in rspec) to automatically generate factories, you may not need to generate associations, because that sets them for you. If you do choose to use associations, note that these will likely create factories with interdependence issues. When there are NOT NULLs on foreign keys and/or presence validations, etc. you can't just use after(:create) or after(:build) to set associations, and without those you can have issues with "Trait not registered" or "Factory not registered". Later versions of FactoryGirl may make this easier, and be sure to see notes from Josh down in the troubleshooting section.

If you are ready to edit factories, copy and paste stuff, rename things, etc. instead of just using Stepford::FactoryGirl or deep_* methods in rspec, then keep reading.

# Include Required Assocations

To include NOT NULL foreign key associations or presence validated associations:

bundle exec stepford factories --include-required-associations
# Include All Associations

To include all associations even if they aren't deemed to be required by not null ActiveRecord constraints defined in the model:

bundle exec stepford factories --associations
# Checking Model Associations

If --associations or --validate-associations is specified, Stepford first loads Rails and attempts to check your models for broken associations.

If associations are deemed broken, it will output proposed changes.

No IDs

If working with a legacy schema, you may have models with foreign_key columns that you don't have associations defined for in the model. If that is the case, we don't want to assign arbitrary integers to them and try to create a record. If that is the case, try --exclude-all-ids, which will exclude those ids as attributes defined in the factories and you can add associations as needed to get things working.

Traits

To generate traits for each attribute that would be included with --attributes, but isn't because --attributes is not specified:

bundle exec stepford factories --attribute-traits

To generate traits for each association that would be included with --associations, but isn't because --associations is not specified:

bundle exec stepford factories --association-traits
Constraints and Validations

If the ActiveRecord column null property for the attribute is true for the attribute or foreign key for the association, or if there is a presence validator for an attribute or foreign key for the association, then that attribute or association will be defined by the default factory.

Uniqueness constraints on the model are handled by the following being generated in the factory, which works for strings and numbers:

sequence(:my_attribute)

If you have a formatting constraint, some other constraint, or don't like the format of the data in the factories, see the Factory Girl documentation to find out how to customize your factories.

Table Sequences

If a table has no sequence, each primary key will get a FactoryGirl sequence, e.g. if you had a tie table with two sequenceless primary key columns, 'a_id' and 'b_id', it will put this in the factory:

sequence(:a_id)
sequence(:b_id)
Composite Primary Keys

You can use the composite_primary_keys gem and it should work fine.

Testing Factories

See Testing all Factories (with RSpec) in the FactoryGirl wiki.

Here is a version that tests the FactoryGirl factories and the Stepford deep_creates:

require 'spec_helper'
require 'stepford/factory_girl/rspec_helpers'

describe 'validate factories build' do
  FactoryGirl.factories.each do |factory|
    context "with factory for :#{factory.name}" do
      subject { deep_create(factory.name) }

      it "is valid" do
        subject.valid?.should be, subject.errors.full_messages.join(',')
      end
    end
  end
end
Troubleshooting

First, please use modelist to help test your models and the backing schema to ensure everything is kosher.

If you have duplicate factory definitions during Rails load, it may complain. Just move, rename, or remove the offending files and factories and retry.

The factories CLI produces factories that use Ruby 1.9 hash syntax. If you aren't using Ruby 1.9, it may not fail during generation, but it might later when loading the factories.

If you are using STI, you'll need to manually fix the value that goes into the type attribute.

If you use Stepford to create factories for existing tests and the tests fail with something like:

 ActiveRecord::StatementInvalid:
   PG::Error: ERROR:  null value in column "something_id" violates not-null constraint

or maybe:

 ActiveRecord::RecordInvalid:
   Validation failed: Item The item is required., Pricer The pricer is required., Purchased by A purchaser is required.

then try to use the deep_* methods to build or create. If still you get an error like:

 ActiveRecord::StatementInvalid:
   PG::Error: ERROR:  null value in column "something_id" violates not-null constraint
   : INSERT INTO "foobars"

ensure that the belongs_to association on the model (e.g. Foobar) is using the proper column name. It may need to explicitly set the :foreign_key option.

Stepford needs some help fixing factories for some validations, for example, if attribute foobar on SomeModel can only be "foo" or "bar", then you may get:

 ActiveRecord::RecordInvalid:
   Validation failed: SomeModel invalid foobar Test Foobar (...)

In which case you need to hand-edit the some_model factory to set the foobar attribute to "foo" or "bar". Keep in mind that if you have a default set for it in the schema, that will be set as part of stepford factories file generation. You may also want to set default values in your models like this if you can't set a default value in the column in the DB schema itself like the example in this answer in StackOverflow:

after_initialize :init
def init
  self.foobar = 'foo'
end

If you get:

SystemStackError:
  stack level too deep

then note that associations and traits can lead to circular dependencies. Trying generating factories without associations or traits (the default), and use the deep_* methods to create.

ThoughtBot's Josh Clayton provided some suggestions for this, including using methods to generate more complex object structures:

def post_containing_comment_by_author
  author = FactoryGirl.create(:user)
  post = FactoryGirl.create(:post)
  FactoryGirl.create_list(:comment, 3)
  FactoryGirl.create(:comment, author: author, post: post)
  post.reload
end

(Note, the deep_* methods that do this automatically for you, including the reloads.)

or referring to created objects through associations, though he said multiple nestings get tricky:

factory :post do
  author
  title 'Ruby is fun'
end

factory :comment do
  author
  post
  body 'I love Ruby too!'

  trait :authored_by_post_author do
    author { post.author }
  end
end

comment = FactoryGirl.create(:comment, :authored_by_post_author)
comment.author == comment.post.author # true

License

Copyright (c) 2012 Gary S. Weaver, released under the MIT license.