1
0
Fork 0
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:
Daniel Colson 2020-07-09 12:26:23 -04:00 committed by GitHub
parent 158e0948e5
commit 9879060289
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 226 additions and 60 deletions

View file

@ -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
--------- ---------

View file

@ -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