FactoryDefault
FactoryDefault aims to help you cope with factory cascades (see FactoryProf) by reusing associated records.
It can be very useful when you're working on a typical SaaS application (or other hierarchical data).
Consider an example. Assume we have the following factories:
factory :account do
end
factory :user do
account
end
factory :project do
account
user
end
factory :task do
account
project
user
endOr in case of Fabrication:
Fabricator(:account) do
end
Fabricator(:user) do
account
end
# etc.And we want to test the Task model:
describe "PATCH #update" do
let(:task) { create(:task) }
it "works" do
patch :update, id: task.id, task: {completed: "t"}
expect(response).to be_success
end
# ...
endHow many users and accounts are created per example? Two and four respectively.
And it breaks our logic (every object should belong to the same account).
Typical workaround:
describe "PATCH #update" do
let(:account) { create(:account) }
let(:project) { create(:project, account: account) }
let(:task) { create(:task, project: project, account: account) }
it "works" do
patch :update, id: task.id, task: {completed: "t"}
expect(response).to be_success
end
endThat works. And there are some cons: it's a little bit verbose and error-prone (easy to forget something).
Here is how we can deal with it using FactoryDefault:
describe "PATCH #update" do
let(:account) { create_default(:account) }
let(:project) { create_default(:project) }
let(:task) { create(:task) }
# and if we need more projects, users, tasks with the same parent record,
# we just write
let(:another_project) { create(:project) } # uses the same account
let(:another_task) { create(:task) } # uses the same account
it "works" do
patch :update, id: task.id, task: {completed: "t"}
expect(response).to be_success
end
endNOTE. This feature introduces a bit of magic to your tests, so use it with caution ('cause tests should be human-readable first). Good idea is to use defaults for top-level entities only (such as tenants in multi-tenancy apps).
Instructions
In your spec_helper.rb:
require "test_prof/recipes/rspec/factory_default"This adds the following methods to FactoryBot and/or Fabrication:
FactoryBot#set_factory_default(factory, object)/Fabricate.set_fabricate_default(factory, object)– use theobjectas default for associations built withfactory.
Example:
let(:user) { create(:user) }
before { FactoryBot.set_factory_default(:user, user) }
# You can also set the default factory with traits
FactoryBot.set_factory_default([:user, :admin], admin)
# Or (since v1.4)
FactoryBot.set_factory_default(:user, :admin, admin)
# You can also register a default record for specific attribute overrides
Fabricate.set_fabricate_default(:post, post, state: "draft")FactoryBot#create_default(...)/Fabricate.create_default(...)– is a shortcut forcreate+set_factory_default.FactoryBot#get_factory_default(factory)/Fabricate.get_fabricate_default(factory)– retrieves the default value forfactory(since v1.4).
# This method also supports traits
admin = FactoryBot.get_factory_default(:user, :admin)IMPORTANT: Defaults are cleaned up after each example by default (i.e., when using test_prof/recipes/rspec/factory_default).
Using with before_all / let_it_be
Defaults created within before_all and let_it_be are not reset after each example, but only at the end of the corresponding example group. So, it's possible to call create_default within let_it_be without any additional configuration. RSpec only
IMPORTANT: You must load FactoryDefault after loading BeforeAll to make this feature work.
NOTE. Regular before(:all) callbacks are not supported.
Working with traits
You can use traits in your associations, for example:
factory :comment do
user
end
factory :post do
association :user, factory: %i[user able_to_post]
end
factory :view do
association :user, factory: %i[user unable_to_post_only_view]
endIf there is a default value for the user factory, it's gonna be used independently of traits. This may break your logic.
To prevent this, configure FactoryDefault to preserve traits:
# Globally
TestProf::FactoryDefault.configure do |config|
config.preserve_traits = true
end
# or in-place
create_default(:user, preserve_traits: true)Creating a default with trait works as follows:
# Create a default with trait
user = create_default(:user_poster, :able_to_post)
# When an association has no traits specified, the default with trait is used
create(:comment).user == user #=> true
# When an association has the matching trait specified, the default is used, too
create(:post).user == user #=> true
# When the association's trait differs, default is skipped
create(:view).user == user #=> falseHandling attribute overrides
It's possible to define attribute overrides for associations:
factory :post do
association :user, name: "Poster"
end
factory :view do
association :user, name: "Viewer"
endFactoryDefault ignores such overrides and still returns a default user record (if created). You can turn the attribute awareness feature on to skip the default record if overrides don't match the default object attributes:
# Globally
TestProf::FactoryDefault.configure do |config|
config.preserve_attributes = true
end
# or in-place
create_default :user, preserve_attributes: trueNOTE: In the future versions of Test Prof, both preserve_traits and preserve_attributes will default to true. We recommend settings them to true if you just starting using this feature.
Ignoring default factories
You can temporary disable the defaults usage by wrapping a code with the skip_factory_default method:
account = create_default(:account)
another_account = skip_factory_default { create(:account) }
expect(another_account).not_to eq(account)Showing usage stats
You can display the FactoryDefault usage stats by setting the FACTORY_DEFAULT_SUMMARY=1 or FACTORY_DEFAULT_STATS=1 env vars or by setting the configuration values:
TestProf::FactoryDefault.configure do |config|
config.report_summary = true
# Report stats prints the detailed usage information (including summary)
config.report_stats = true
endFor example:
$ FACTORY_DEFAULT_SUMMARY=1 bundle exec rspec
FactoryDefault summary: hit=11 miss=3Where hit indicates the number of times the default factory value was used instead of a new one when an association was created; miss indicates the number of time the default value was ignored due to traits or attributes mismatch.
Factory Default profiling, or when to use defaults
Factory Default ships with the profiler, which can help you to see how associations are being used in your test suite, so you can decide on using create_default or not.
To enable profiling, run your tests with the FACTORY_DEFAULT_PROF=1 set:
$ FACTORY_DEFAULT_PROF=1 bundle exec rspec spec/some/file_spec.rb
.....
[TEST PROF INFO] Factory associations usage:
factory count total time
user 17 00:42.010
user[traited] 15 00:31.560
user{tag:"some tag"} 1 00:00.205
Total associations created: 33
Total uniq associations created: 3
Total time spent: 01:13.775Since default factories are usually registered per an example group (or test class), we recommend running this profiler against a particular file, so you can quickly identify the possibility of adding create_default and improve the tests speed.
NOTE: You can also use the profiler to measure the effect of adding create_default; for that, compare the results of running the profiler with FactoryDefault enabled and disabled (you can do that by passing the FACTORY_DEFAULT_DISABLED=1 env var).