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!
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
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)
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 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
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)
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
Add somewhere after the require:
Stepford::FactoryGirl.debug = true
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
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
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
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.
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.
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
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.
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)
You can use the composite_primary_keys gem and it should work fine.
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
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
Copyright (c) 2012 Gary S. Weaver, released under the MIT license.