1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
|
# FactoryDefault
_FactoryDefault_ aims to help you cope with _factory cascades_ (see [FactoryProf](../profilers/factory_prof.md)) 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:
```ruby
factory :account do
end
factory :user do
account
end
factory :project do
account
user
end
factory :task do
account
project
user
end
```
Or in case of Fabrication:
```ruby
Fabricator(:account) do
end
Fabricator(:user) do
account
end
# etc.
```
And we want to test the `Task` model:
```ruby
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
# ...
end
```
How 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:
```ruby
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
end
```
That 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:
```ruby
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
end
```
**NOTE**. 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`:
```ruby
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 the `object` as default for associations built with `factory`.
Example:
```ruby
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 for `create` + `set_factory_default`.
- `FactoryBot#get_factory_default(factory)` / `Fabricate.get_fabricate_default(factory)` – retrieves the default value for `factory` (since v1.4).
```rb
# 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:
```ruby
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]
end
```
If 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:
```ruby
# 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:
```ruby
# 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 #=> false
```
### Handling attribute overrides
It's possible to define attribute overrides for associations:
```ruby
factory :post do
association :user, name: "Poster"
end
factory :view do
association :user, name: "Viewer"
end
```
FactoryDefault 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:
```ruby
# Globally
TestProf::FactoryDefault.configure do |config|
config.preserve_attributes = true
end
# or in-place
create_default :user, preserve_attributes: true
```
**NOTE:** 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:
```ruby
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:
```ruby
TestProf::FactoryDefault.configure do |config|
config.report_summary = true
# Report stats prints the detailed usage information (including summary)
config.report_stats = true
end
```
For example:
```sh
$ FACTORY_DEFAULT_SUMMARY=1 bundle exec rspec
FactoryDefault summary: hit=11 miss=3
```
Where `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:
```sh
$ 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.775
```
Since 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).
|