mirror of
https://github.com/thoughtbot/factory_bot.git
synced 2022-11-09 11:43:51 -05:00
Improve documentation for collection associations (#1420)
* Improve documentation for collection associations This commit makes `Evaluator#association` and `Evaluator#instance` officially part of the public API of factory\_bot. Folks were already using these methods, and all of the examples here were inspired by examples in the various issues mentioned below. This commit also adds and updates some tests to match the examples in the documentation, to ensure that these examples will continue to work as expected. Closes #1268 by adding documentation for both `Evaluator#association` and `Evaluator#instance`. The documentation on interconnected associations is also relevant to #1063, #1255, and #1309. Closes #1304 by providing multiple alternative approaches to creating collection associations, and adding a note about reloading the record in the callback example. This had also come up back in #549. Closes #458 by offering `Evaluator#association` as a more flexible way to build collection associations. This has come up many times over the years in many different forms, including #426, #487, #640, #1022, #1150, and #1360.
This commit is contained in:
parent
158e0948e5
commit
9879060289
2 changed files with 226 additions and 60 deletions
|
@ -44,6 +44,7 @@ Getting Started
|
||||||
+ [`has_many` associations](#has_many-associations)
|
+ [`has_many` associations](#has_many-associations)
|
||||||
+ [`has_and_belongs_to_many` associations](#has_and_belongs_to_many-associations)
|
+ [`has_and_belongs_to_many` associations](#has_and_belongs_to_many-associations)
|
||||||
+ [Polymorphic associations](#polymorphic-associations)
|
+ [Polymorphic associations](#polymorphic-associations)
|
||||||
|
+ [Interconnected associations](#interconnected-associations)
|
||||||
* [Sequences](#sequences)
|
* [Sequences](#sequences)
|
||||||
+ [Global sequences](#global-sequences)
|
+ [Global sequences](#global-sequences)
|
||||||
+ [With dynamic attributes](#with-dynamic-attributes)
|
+ [With dynamic attributes](#with-dynamic-attributes)
|
||||||
|
@ -653,27 +654,51 @@ factory :post do
|
||||||
|
|
||||||
### `has_many` associations
|
### `has_many` associations
|
||||||
|
|
||||||
Generating data for a `has_many` relationship is a bit more involved,
|
There are a few ways to generate data for a `has_many` relationship. The
|
||||||
depending on the amount of flexibility desired, but here's a surefire example
|
simplest approach is to write a helper method in plain Ruby to tie together the
|
||||||
of generating associated data.
|
different records:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :post do
|
||||||
|
title { "Through the Looking Glass" }
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
factory :user do
|
||||||
|
name { "Rachel Sanchez" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_with_posts(posts_count: 5)
|
||||||
|
FactoryBot.create(:user) do |user|
|
||||||
|
FactoryBot.create_list(:post, posts_count, user: user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
create(:user).posts.length # 0
|
||||||
|
user_with_posts.posts.length # 5
|
||||||
|
user_with_posts(posts_count: 15).posts.length # 15
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer to keep the object creation fully within factory\_bot, you can
|
||||||
|
build the posts in an `after(:create)` callback.
|
||||||
|
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
|
|
||||||
# post factory with a `belongs_to` association for the user
|
|
||||||
factory :post do
|
factory :post do
|
||||||
title { "Through the Looking Glass" }
|
title { "Through the Looking Glass" }
|
||||||
user
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
# user factory without associated posts
|
|
||||||
factory :user do
|
factory :user do
|
||||||
name { "John Doe" }
|
name { "John Doe" }
|
||||||
|
|
||||||
# user_with_posts will create post data after the user has been created
|
# user_with_posts will create post data after the user has been created
|
||||||
factory :user_with_posts do
|
factory :user_with_posts do
|
||||||
# posts_count is declared as a transient attribute and available in
|
# posts_count is declared as a transient attribute avialabile in the
|
||||||
# attributes on the factory, as well as the callback via the evaluator
|
# callback via the evaluator
|
||||||
transient do
|
transient do
|
||||||
posts_count { 5 }
|
posts_count { 5 }
|
||||||
end
|
end
|
||||||
|
@ -684,71 +709,122 @@ FactoryBot.define do
|
||||||
# to create and we make sure the user is associated properly to the post
|
# to create and we make sure the user is associated properly to the post
|
||||||
after(:create) do |user, evaluator|
|
after(:create) do |user, evaluator|
|
||||||
create_list(:post, evaluator.posts_count, user: user)
|
create_list(:post, evaluator.posts_count, user: user)
|
||||||
|
|
||||||
|
# You may need to reload the record here, depending on your application
|
||||||
|
user.reload
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
```
|
|
||||||
|
|
||||||
This allows us to do:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
create(:user).posts.length # 0
|
create(:user).posts.length # 0
|
||||||
create(:user_with_posts).posts.length # 5
|
create(:user_with_posts).posts.length # 5
|
||||||
create(:user_with_posts, posts_count: 15).posts.length # 15
|
create(:user_with_posts, posts_count: 15).posts.length # 15
|
||||||
```
|
```
|
||||||
|
|
||||||
### `has_and_belongs_to_many` associations
|
Or, for a solution that works with `build`, `build_stubbed`, and `create`
|
||||||
|
(although it doesn't work well with `attributes_for`), you can use inline
|
||||||
Generating data for a `has_and_belongs_to_many` relationship is very similar
|
associations:
|
||||||
to the above `has_many` relationship, with a small change, you need to pass an
|
|
||||||
array of objects to the model's pluralized attribute name rather than a single
|
|
||||||
object to the singular version of the attribute name.
|
|
||||||
|
|
||||||
Here's an example with two models that are related via
|
|
||||||
`has_and_belongs_to_many`:
|
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
|
factory :post do
|
||||||
# language factory with a `belongs_to` association for the profile
|
|
||||||
factory :language do
|
|
||||||
title { "Through the Looking Glass" }
|
title { "Through the Looking Glass" }
|
||||||
profile
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
# profile factory without associated languages
|
factory :user do
|
||||||
factory :profile do
|
name { "Taylor Kim" }
|
||||||
name { "John Doe" }
|
|
||||||
|
|
||||||
# profile_with_languages will create language data after the profile has
|
factory :user_with_posts do
|
||||||
# been created
|
posts { [association(:post)] }
|
||||||
factory :profile_with_languages do
|
end
|
||||||
# languages_count is declared as an ignored attribute and available in
|
end
|
||||||
# attributes on the factory, as well as the callback via the evaluator
|
end
|
||||||
|
|
||||||
|
create(:user).posts.length # 0
|
||||||
|
create(:user_with_posts).posts.length # 1
|
||||||
|
build(:user_with_posts).posts.length # 1
|
||||||
|
build_stubbed(:user_with_posts).posts.length # 1
|
||||||
|
```
|
||||||
|
|
||||||
|
For more flexibility you can combine this with the `posts_count` transient
|
||||||
|
attribute from the callback example:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :post do
|
||||||
|
title { "Through the Looking Glass" }
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
factory :user do
|
||||||
|
name { "Adiza Kumato" }
|
||||||
|
|
||||||
|
factory :user_with_posts do
|
||||||
transient do
|
transient do
|
||||||
languages_count { 5 }
|
posts_count { 5 }
|
||||||
end
|
end
|
||||||
|
|
||||||
# the after(:create) yields two values; the profile instance itself and
|
posts do
|
||||||
# the evaluator, which stores all values from the factory, including
|
Array.new(posts_count) { association(:post) }
|
||||||
# ignored attributes; `create_list`'s second argument is the number of
|
|
||||||
# records to create and we make sure the profile is associated properly
|
|
||||||
# to the language
|
|
||||||
after(:create) do |profile, evaluator|
|
|
||||||
create_list(:language, evaluator.languages_count, profiles: [profile])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create(:user_with_posts).posts.length # 5
|
||||||
|
create(:user_with_posts, posts_count: 15).posts.length # 15
|
||||||
|
build(:user_with_posts, posts_count: 15).posts.length # 15
|
||||||
|
build_stubbed(:user_with_posts, posts_count: 15).posts.length # 15
|
||||||
```
|
```
|
||||||
|
|
||||||
This allows us to do:
|
### `has_and_belongs_to_many` associations
|
||||||
|
|
||||||
|
Generating data for a `has_and_belongs_to_many` relationship is very similar
|
||||||
|
to the above `has_many` relationship, with a small change: you need to pass an
|
||||||
|
array of objects to the model's pluralized attribute name rather than a single
|
||||||
|
object to the singular version of the attribute name.
|
||||||
|
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
create(:profile).languages.length # 0
|
def profile_with_languages(languages_count: 2)
|
||||||
create(:profile_with_languages).languages.length # 5
|
FactoryBot.create(:profile) do |profile|
|
||||||
create(:profile_with_languages, languages_count: 15).languages.length # 15
|
FactoryBot.create_list(:language, languages_count, profiles: [profile])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with the callback approach:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
factory :profile_with_languages do
|
||||||
|
transient do
|
||||||
|
languages_count { 2 }
|
||||||
|
end
|
||||||
|
|
||||||
|
after(:create) do |profile, evaluator|
|
||||||
|
create_list(:language, evaluator.languages_count, profiles: [profile])
|
||||||
|
profile.reload
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Or the inline association approach (note the use of the `instance` method here
|
||||||
|
to refer to the profile being built):
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
factory :profile_with_languages do
|
||||||
|
transient do
|
||||||
|
languages_count { 2 }
|
||||||
|
end
|
||||||
|
|
||||||
|
languages do
|
||||||
|
Array.new(languages_count) do
|
||||||
|
association(:language, profiles: [instance])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
### Polymorphic associations
|
### Polymorphic associations
|
||||||
|
@ -782,6 +858,57 @@ create(:comment, :for_video)
|
||||||
create(:comment, :for_photo)
|
create(:comment, :for_photo)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Interconnected associations
|
||||||
|
|
||||||
|
There are limitless ways objects might be interconnected, and
|
||||||
|
factory\_bot may not always be suited to handle those relationships. In some
|
||||||
|
cases it makes sense to use factory\_bot to build each individual object, and
|
||||||
|
then to write helper methods in plain Ruby to tie those objects together.
|
||||||
|
|
||||||
|
That said, some more complex, interconnected relationships can be built in factory\_bot
|
||||||
|
using inline associations with reference to the `instance` being built.
|
||||||
|
|
||||||
|
Let's say your models look like this, where an associated `Student` and
|
||||||
|
`Profile` should both belong to the same `School`:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class Student < ApplicationRecord
|
||||||
|
belongs_to :school
|
||||||
|
has_one :profile
|
||||||
|
end
|
||||||
|
|
||||||
|
class Profile < ApplicationRecord
|
||||||
|
belongs_to :school
|
||||||
|
belongs_to :student
|
||||||
|
end
|
||||||
|
|
||||||
|
class School < ApplicationRecord
|
||||||
|
has_many :students
|
||||||
|
has_many :profiles
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
We can ensure the student and profile are connected to each other and to the
|
||||||
|
same school with a factory like this:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :student do
|
||||||
|
school
|
||||||
|
profile { association :profile, student: instance, school: school }
|
||||||
|
end
|
||||||
|
|
||||||
|
factory :profile do
|
||||||
|
school
|
||||||
|
student { association :student, profile: instance, school: school }
|
||||||
|
end
|
||||||
|
|
||||||
|
factory :school
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this approach works with `build`, `build_stubbed`, and `create`, but
|
||||||
|
the associations will return `nil` when using `attributes_for`.
|
||||||
|
|
||||||
Sequences
|
Sequences
|
||||||
---------
|
---------
|
||||||
|
|
|
@ -59,48 +59,87 @@ describe "associations" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "connects records with interdependent relationships" do
|
it "connects records with interdependent relationships" do
|
||||||
define_model("User", school_id: :integer) do
|
define_model("Student", school_id: :integer) do
|
||||||
belongs_to :school
|
belongs_to :school
|
||||||
has_one :profile
|
has_one :profile
|
||||||
end
|
end
|
||||||
|
|
||||||
define_model("Profile", school_id: :integer, user_id: :integer) do
|
define_model("Profile", school_id: :integer, student_id: :integer) do
|
||||||
belongs_to :school
|
belongs_to :school
|
||||||
belongs_to :user
|
belongs_to :student
|
||||||
end
|
end
|
||||||
|
|
||||||
define_model("School") do
|
define_model("School") do
|
||||||
has_many :users
|
has_many :students
|
||||||
has_many :profiles
|
has_many :profiles
|
||||||
end
|
end
|
||||||
|
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :user do
|
factory :student do
|
||||||
school
|
school
|
||||||
profile { association :profile, user: instance, school: school }
|
profile { association :profile, student: instance, school: school }
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :profile do
|
factory :profile do
|
||||||
school
|
school
|
||||||
user { association :user, profile: instance, school: school }
|
student { association :student, profile: instance, school: school }
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :school
|
factory :school
|
||||||
end
|
end
|
||||||
|
|
||||||
user = FactoryBot.create(:user)
|
student = FactoryBot.create(:student)
|
||||||
|
|
||||||
expect(user.profile.school).to eq(user.school)
|
expect(student.profile.school).to eq(student.school)
|
||||||
expect(user.profile.user).to eq(user)
|
expect(student.profile.student).to eq(student)
|
||||||
expect(user.school.users.map(&:id)).to eq([user.id])
|
expect(student.school.students.map(&:id)).to eq([student.id])
|
||||||
expect(user.school.profiles.map(&:id)).to eq([user.profile.id])
|
expect(student.school.profiles.map(&:id)).to eq([student.profile.id])
|
||||||
|
|
||||||
profile = FactoryBot.create(:profile)
|
profile = FactoryBot.create(:profile)
|
||||||
|
|
||||||
expect(profile.user.school).to eq(profile.school)
|
expect(profile.student.school).to eq(profile.school)
|
||||||
expect(profile.user.profile).to eq(profile)
|
expect(profile.student.profile).to eq(profile)
|
||||||
expect(profile.school.profiles.map(&:id)).to eq([profile.id])
|
expect(profile.school.profiles.map(&:id)).to eq([profile.id])
|
||||||
expect(profile.school.users.map(&:id)).to eq([profile.user.id])
|
expect(profile.school.students.map(&:id)).to eq([profile.student.id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when building collection associations" do
|
||||||
|
it "builds the association according to the given strategy" do
|
||||||
|
define_model("Photo", listing_id: :integer) do
|
||||||
|
belongs_to :listing
|
||||||
|
attr_accessor :name
|
||||||
|
end
|
||||||
|
|
||||||
|
define_model("Listing") do
|
||||||
|
has_many :photos
|
||||||
|
end
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :photo
|
||||||
|
|
||||||
|
factory :listing do
|
||||||
|
photos { [association(:photo)] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
created_listing = FactoryBot.create(:listing)
|
||||||
|
|
||||||
|
expect(created_listing.photos.first).to be_a Photo
|
||||||
|
expect(created_listing.photos.first).to be_persisted
|
||||||
|
|
||||||
|
built_listing = FactoryBot.build(:listing)
|
||||||
|
|
||||||
|
expect(built_listing.photos.first).to be_a Photo
|
||||||
|
expect(built_listing.photos.first).not_to be_persisted
|
||||||
|
|
||||||
|
stubbed_listing = FactoryBot.build_stubbed(:listing)
|
||||||
|
|
||||||
|
expect(stubbed_listing.photos.first).to be_a Photo
|
||||||
|
expect(stubbed_listing.photos.first).to be_persisted
|
||||||
|
expect { stubbed_listing.photos.first.save! }.to raise_error(
|
||||||
|
"stubbed models are not allowed to access the database - Photo#save!()"
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue