Make your tests fast by augmenting them with transactional fixtures powered by your favorite factory library!
We all know that ActiveRecord's fixtures are hard to maintain, and more importantly, disconnected from the models that create your data. This disconnect can lead to invalid or incomplete representations of your objects as your application grows. But Rails did get something right. Fixtures combined with transactional tests are a huge performance win while providing canned references among your team via helper methods that find fixtures using a unique name. The NamedSeeds gem aims to replace YAML fixtures by providing a slim identification layer to be used in conjunction with your factory library of choice. For example, FactoryGirl.
The idea is to leverage your tests' existing factories to generate fixtures that will be populated before testing starts and to use a database transaction strategy around each test. In this way you have a curated set of personas that can be accessed via convenient helper methods like users(:admin)
which in turn yields much faster test runs. NamedSeeds fixtures also become your seed data for Rails' development environment. This consistency between development and testing is a huge win for on-boarding new team members. Lastly, database fixtures, even those seeded by factories, are not a panacea and we recommend that you continue to use factories in your tests for edge cases when it makes sense to do so.
NamedSeeds is best when your factories follow two key principals:
- Leveraging your models for most business logic.
- Creation of "valid garbage" with no/minimal args while allowing idempotency via explicit args.
Add the gem to your Rails' Gemfile in both the development and test group as shown below. This is needed since the NamedSeeds gem has integrations in both environments.
group :development, :test do
gem 'named_seeds'
end
NamedSeeds only requires that you customize the Rails db/seeds.rb
file. The contents of this file can be anything you want! We recommend using a factory library like FactoryGirl or Machinist.
require 'factory_girl'
include FactoryGirl::Syntax::Methods
FactoryGirl.find_definitions rescue false
@bob = create :user, id: NamedSeeds.identify(:bob), email: 'bob@test.com'
In this example we have given our Bob user an explicit primary key using the identify method of NamedSeeds. This ensures we have a handle on Bob in our tests. For this happen, make the following changes to your Rails test_helper.rb
file.
ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
NamedSeeds.load_seed
class ActiveSupport::TestCase
named_seeds :users
end
Here we have added NamedSeeds.load_seed
after our requires. This ensures our test database is properly seeded. We have also added a named_seeds :users
declaration that creates a test helper method that can find any persona with a matching identity. The method name follows ActiveRecord model/table name conventions. So in this case we expect to find a User
model. An example of what our unit test for Bob might look like with the new users
fixture helper would be:
require 'test_helper'
class UserTest < ActiveSupport::TestCase
tests "should work" do
user = users(:bob)
assert_equal 'bob@test.com', user.email
end
end
This test is very contrived and is only meant to illustrate the users(:bob)
test helper which closely mimics ActiveRecord's fixture helpers.
NamedSeeds hooks into the db:setup
process. In this way, the same fixture story seeded in your test environment is the same in development. For example, if you are on-boarding a new developer and bootstrapping their environment.
$ bundle
$ rake db:create:all db:setup
Your local development database will now contain all fixtures created in db/seeds. As you fixtures grow, re-create your development environment with confidence.
$ bundle
$ rake db:drop:all db:create:all db:setup
If you do not like typing NamedSeeds.identify(:persona)
over and over again in your seeds file, you can include the NamedSeeds::DSL
at the top. This will give you direct scope to the identify
method which is also succinctly aliased to just id
if you want.
include NamedSeeds::DSL
id(:ruby) # => 207281424
id(:sapphire_2) # => 1066363776
By default identities boil down to a consistent integer whose values are less than 2^30. These are great for typical primary or foreign keys. Now that Rails supports UUID keys in both models and ActiveRecord fixtures, so does the NamedSeeds gem. The identity method can use an optional second parameter to denote the SQL type when seeding your database.
id(:ken, :uuid) # => '4f156606-8cb3-509e-a177-956ca0a22015'
So if our User
model did use a UUID column type, our seed file might look like this.
@bob = create :user, id: NamedSeeds.identify(:bob), email: 'bob@test.com'
If your model uses strings for primary keys or what is known as "natural" keys, then NamedSeeds can still work for you. First, create your seed data without the identity helper. For example, below we are seeding US states in a contrived lookup table.
# In db/seeds.rb file.
create :state, id: 'VA', name: 'Virginia'
create :state, id: 'WA', name: 'Washington'
Here the primary key of the State
model is a string column. You have two options to generate test helpers for these objects. Note that none are technically needed since the identities are known and not random integers.
named_seeds :states, identities: {virginia: 'VA', washington: 'WA'}
states(:virginia) # => #<State id: 'VA'... >
named_seeds :states, identities: :natural
states('VA') # => #<State id: 'VA'... >
By passing an identities hash to named_seeds
you can customize the name of the key used in finders. The first line would generate a method that allows you to find states using the longer identities key values. This is great if your natural keys do not necessarily speak to the persona/object under test. If you wanted to use the natural key string values, you can just pass :natural
to the identities option.
If your test helper name can not infer the class name, just use the :class
option when declaring seeds in your test helper.
named_seeds :users, class: Legacy::User
Unlike ActiveRecord fixtures, NamedSeeds does not support loading fixtures per test case. Since the entire fixture story is loaded before the test suite runs. NamedSeeds is much more akin to fixtures(:all)
and the named_seeds
method is more about creating test helper methods to access named fixtures. Lastly, NamedSeeds has no notion of instantiated fixtures.
Rails 4 (maybe starting in 4.1) now has a new test database synchronization strategy. No longer is your entire development schema cloned when you run your tests. This saves time when running tests but it also adds a potential gotcha when using db/seeds.rb with the NamedSeeds gem. We recommend using the DatabaseCleaner gem and cleaning your DB at the top of your seed file.
# In your Gemfile.
group :development, :test do
gem 'named_seeds'
gem 'database_cleaner'
end
# Top of db/seeds.rb file.
require 'database_cleaner'
DatabaseCleaner.clean_with :truncation
DatabaseCleaner.clean
All configurations are best done in a config/initializers/named_seeds.rb
file using a defined check as shown below. All other examples below assume this convention.
if defined?(NamedSeeds)
Rails.application.config.named_seeds
end
Rails::Engines are a great way to distribute shared functionality. Rails exposes a hook called load_seed
that an engine can implement. These are great for seeding tables that an engine's models may need. You can tell NamedSeeds to use any object that responds to load_seed
and each will be called after db/seeds.rb
is loaded. For example:
config.named_seeds.engines_with_load_seed = [
GeoData::Engine,
OurLookupTables
]
By default, the NamedSeeds gem relies on Rails db/seeds.rb
as your seed file. Some people use this file for setting up new production instances while others have code in this file that is something completely different. If this file is not safe for development/test then you should really re-examine your usage of db/seeds.rb
since Rails loads this file on the db:setup
task automatically. If you do not want to use this for your development/test seed data, then you can customize the location. However, NamedSeeds does this by running our own named_seeds:setup
task after the Rails db:setup
task. So the result is still somewhat the same for development but this does keep db/seeds.rb
out of the test environment.
config.named_seeds.load_app_seed_file = false
config.named_seeds.custom_seed_file = 'db/seeds_devtest.rb'
The current master branch is for Rails v4.0.0 and up and. Please use our 1-0-stable
branch for Rails v3.x.
We use the Appraisal gem from Thoughtbot to help us test different versions of Rails. The rake appraisal test
command actually runs our test suite against all Rails versions in our Appraisal
file. So after cloning the repo, running the following commands.
$ bundle install
$ bundle exec appraisal install
$ bundle exec appraisal rake test
If you want to run the tests for a specific Rails version, use one of the appraisal names found in our Appraisals
file. For example, the following will run our tests suite for Rails 4.1.x.
$ bundle exec appraisal rails41 rake test