aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/cases/associations
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/test/cases/associations')
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb30
-rw-r--r--activerecord/test/cases/associations/eager_test.rb22
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb8
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb20
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb36
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb14
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb28
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb38
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb546
9 files changed, 667 insertions, 75 deletions
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 0742e311d9..39e8a7960a 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -15,17 +15,17 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_with_cascaded_two_levels
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
- assert_equal 2, authors.size
+ assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
- assert_equal 1, authors[1].posts.size
+ assert_equal 3, authors[1].posts.size
assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
end
def test_eager_association_loading_with_cascaded_two_levels_and_one_level
authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id")
- assert_equal 2, authors.size
+ assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
- assert_equal 1, authors[1].posts.size
+ assert_equal 3, authors[1].posts.size
assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
assert_equal 1, authors[0].categorizations.size
assert_equal 2, authors[1].categorizations.size
@@ -51,24 +51,24 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors])
assert_nothing_raised do
- assert_equal 2, categories.count
- assert_equal 2, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes
+ assert_equal 3, categories.count
+ assert_equal 3, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes
end
end
def test_cascaded_eager_association_loading_with_duplicated_includes
categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null")
assert_nothing_raised do
- assert_equal 2, categories.count
- assert_equal 2, categories.all.size
+ assert_equal 3, categories.count
+ assert_equal 3, categories.all.size
end
end
def test_cascaded_eager_association_loading_with_twice_includes_edge_cases
categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null")
assert_nothing_raised do
- assert_equal 2, categories.count
- assert_equal 2, categories.all.size
+ assert_equal 3, categories.count
+ assert_equal 3, categories.all.size
end
end
@@ -81,15 +81,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
- assert_equal 2, authors.size
+ assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
- assert_equal 1, authors[1].posts.size
+ assert_equal 3, authors[1].posts.size
assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
end
def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference
authors = Author.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id")
- assert_equal 2, authors.size
+ assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal authors(:david).name, authors[0].name
assert_equal [authors(:david).name], authors[0].posts.collect{|post| post.author.name}.uniq
@@ -157,9 +157,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_where_first_level_returns_nil
authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC')
- assert_equal [authors(:mary), authors(:david)], authors
+ assert_equal [authors(:bob), authors(:mary), authors(:david)], authors
assert_no_queries do
- authors[1].post_about_thinking.comments.first
+ authors[2].post_about_thinking.comments.first
end
end
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 48406a9b98..9bc7910fc6 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -68,8 +68,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_with_ordering
list = Post.find(:all, :include => :comments, :order => "posts.id DESC")
- [:eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments,
- :authorless, :thinking, :welcome
+ [:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other,
+ :sti_habtm, :sti_post_and_comments, :sti_comments, :authorless, :thinking, :welcome
].each_with_index do |post, index|
assert_equal posts(post), list[index]
end
@@ -97,25 +97,25 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle
Post.connection.expects(:in_clause_length).at_least_once.returns(5)
posts = Post.find(:all, :include=>:comments)
- assert_equal 7, posts.size
+ assert_equal 11, posts.size
end
def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
posts = Post.find(:all, :include=>:comments)
- assert_equal 7, posts.size
+ assert_equal 11, posts.size
end
def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle
Post.connection.expects(:in_clause_length).at_least_once.returns(5)
posts = Post.find(:all, :include=>:categories)
- assert_equal 7, posts.size
+ assert_equal 11, posts.size
end
def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
posts = Post.find(:all, :include=>:categories)
- assert_equal 7, posts.size
+ assert_equal 11, posts.size
end
def test_load_associated_records_in_one_query_when_adapter_has_no_limit
@@ -170,6 +170,16 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal [comment], category.posts[0].comments
end
end
+
+ def test_associations_loaded_for_all_records
+ post = Post.create!(:title => 'foo', :body => "I like cars!")
+ comment = SpecialComment.create!(:body => 'Come on!', :post => post)
+ first_category = Category.create! :name => 'First!', :posts => [post]
+ second_category = Category.create! :name => 'Second!', :posts => [post]
+
+ categories = Category.where(:id => [first_category.id, second_category.id]).includes(:posts => :special_comments)
+ assert_equal categories.map { |category| category.posts.first.special_comments.loaded? }, [true, true]
+ end
def test_finding_with_includes_on_has_many_association_with_same_include_includes_only_once
author_id = authors(:david).id
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index dc382c3007..f4d14853d3 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -604,7 +604,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
project = SpecialProject.create("name" => "Special Project")
assert developer.save
developer.projects << project
- developer.update_attribute("name", "Bruza")
+ developer.update_column("name", "Bruza")
assert_equal 1, Developer.connection.select_value(<<-end_sql).to_i
SELECT count(*) FROM developers_projects
WHERE project_id = #{project.id}
@@ -642,12 +642,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_find_grouped
all_posts_from_category1 = Post.find(:all, :conditions => "category_id = 1", :joins => :categories)
grouped_posts_of_category1 = Post.find(:all, :conditions => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories)
- assert_equal 4, all_posts_from_category1.size
- assert_equal 1, grouped_posts_of_category1.size
+ assert_equal 5, all_posts_from_category1.size
+ assert_equal 2, grouped_posts_of_category1.size
end
def test_find_scoped_grouped
- assert_equal 4, categories(:general).posts_grouped_by_title.size
+ assert_equal 5, categories(:general).posts_grouped_by_title.size
assert_equal 1, categories(:technology).posts_grouped_by_title.size
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index ad774eb9ce..007f11b535 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -70,16 +70,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# would be convenient), because this would cause that scope to be applied to any callbacks etc.
def test_build_and_create_should_not_happen_within_scope
car = cars(:honda)
- original_scoped_methods = Bulb.scoped_methods
+ scoped_count = car.foo_bulbs.scoped.where_values.count
- bulb = car.bulbs.build
- assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
+ bulb = car.foo_bulbs.build
+ assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
- bulb = car.bulbs.create
- assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
+ bulb = car.foo_bulbs.create
+ assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
- bulb = car.bulbs.create!
- assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
+ bulb = car.foo_bulbs.create!
+ assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
end
def test_no_sql_should_be_fired_if_association_already_loaded
@@ -639,7 +639,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_with_dependent_delete_all
post = posts(:welcome)
- post.update_attribute(:taggings_with_delete_all_count, post.taggings_count)
+ post.update_column(:taggings_with_delete_all_count, post.taggings_count)
assert_difference "post.reload.taggings_with_delete_all_count", -1 do
post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first)
@@ -648,7 +648,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_with_dependent_destroy
post = posts(:welcome)
- post.update_attribute(:taggings_with_destroy_count, post.taggings_count)
+ post.update_column(:taggings_with_destroy_count, post.taggings_count)
assert_difference "post.reload.taggings_with_destroy_count", -1 do
post.taggings_with_destroy.delete(post.taggings_with_destroy.first)
@@ -787,7 +787,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = Firm.find(:first)
# break the vanilla firm_id foreign key
assert_equal 2, firm.clients.count
- firm.clients.first.update_attribute(:firm_id, nil)
+ firm.clients.first.update_column(:firm_id, nil)
assert_equal 1, firm.clients(true).count
assert_equal 1, firm.clients_using_primary_key_with_delete_all.count
old_record = firm.clients_using_primary_key_with_delete_all.first
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index efdecd4b09..70a4e06dbe 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -17,9 +17,10 @@ require 'models/developer'
require 'models/subscriber'
require 'models/book'
require 'models/subscription'
-require 'models/categorization'
-require 'models/category'
require 'models/essay'
+require 'models/category'
+require 'models/owner'
+require 'models/categorization'
require 'models/member'
require 'models/membership'
require 'models/club'
@@ -27,7 +28,7 @@ require 'models/club'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
:owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses,
- :subscribers, :books, :subscriptions, :developers, :categorizations
+ :subscribers, :books, :subscriptions, :developers, :categorizations, :essays
# Dummies to force column loads so query counts are clean.
def setup
@@ -43,7 +44,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_associate_existing
- assert_queries(2) { posts(:thinking); people(:david) }
+ posts(:thinking); people(:david) # Warm cache
assert_queries(1) do
posts(:thinking).people << people(:david)
@@ -285,7 +286,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_update_counter_caches_on_delete_with_dependent_destroy
post = posts(:welcome)
tag = post.tags.create!(:name => 'doomed')
- post.update_attribute(:tags_with_destroy_count, post.tags.count)
+ post.update_column(:tags_with_destroy_count, post.tags.count)
assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do
posts(:welcome).tags_with_destroy.delete(tag)
@@ -295,7 +296,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_update_counter_caches_on_delete_with_dependent_nullify
post = posts(:welcome)
tag = post.tags.create!(:name => 'doomed')
- post.update_attribute(:tags_with_nullify_count, post.tags.count)
+ post.update_column(:tags_with_nullify_count, post.tags.count)
assert_no_difference 'post.reload.taggings_count' do
assert_difference 'post.reload.tags_with_nullify_count', -1 do
@@ -656,6 +657,25 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert author.comments.include?(comment)
end
+ def test_has_many_through_polymorphic_with_primary_key_option
+ assert_equal [categories(:general)], authors(:david).essay_categories
+
+ authors = Author.joins(:essay_categories).where('categories.id' => categories(:general).id)
+ assert_equal authors(:david), authors.first
+
+ assert_equal [owners(:blackbeard)], authors(:david).essay_owners
+
+ authors = Author.joins(:essay_owners).where("owners.name = 'blackbeard'")
+ assert_equal authors(:david), authors.first
+ end
+
+ def test_has_many_through_with_primary_key_option
+ assert_equal [categories(:general)], authors(:david).essay_categories_2
+
+ authors = Author.joins(:essay_categories_2).where('categories.id' => categories(:general).id)
+ assert_equal authors(:david), authors.first
+ end
+
def test_size_of_through_association_should_increase_correctly_when_has_many_association_is_added
post = posts(:thinking)
readers = post.readers.size
@@ -679,10 +699,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_joining_has_many_through_belongs_to
- posts = Post.joins(:author_categorizations).
+ posts = Post.joins(:author_categorizations).order('posts.id').
where('categorizations.id' => categorizations(:mary_thinking_sti).id)
- assert_equal [posts(:eager_other)], posts
+ assert_equal [posts(:eager_other), posts(:misc_by_mary), posts(:other_by_mary)], posts
end
def test_select_chosen_fields_only
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index c1dad5e246..f3c96ccbe6 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -165,16 +165,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_build_and_create_should_not_happen_within_scope
pirate = pirates(:blackbeard)
- original_scoped_methods = Bulb.scoped_methods.dup
+ scoped_count = pirate.association(:foo_bulb).scoped.where_values.count
- bulb = pirate.build_bulb
- assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
+ bulb = pirate.build_foo_bulb
+ assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
- bulb = pirate.create_bulb
- assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
+ bulb = pirate.create_foo_bulb
+ assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
- bulb = pirate.create_bulb!
- assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
+ bulb = pirate.create_foo_bulb!
+ assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
end
def test_create_association
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index bfc5ddc747..968025ade8 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -9,13 +9,16 @@ require 'models/member_detail'
require 'models/minivan'
require 'models/dashboard'
require 'models/speedometer'
+require 'models/category'
require 'models/author'
+require 'models/essay'
+require 'models/owner'
require 'models/post'
require 'models/comment'
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
- :dashboards, :speedometers, :authors, :posts, :comments
+ :dashboards, :speedometers, :authors, :posts, :comments, :categories, :essays, :owners
def setup
@member = members(:groucho)
@@ -87,12 +90,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_with_conditions_eager_loading
# conditions on the through table
assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :favourite_club).favourite_club
- memberships(:membership_of_favourite_club).update_attribute(:favourite, false)
+ memberships(:membership_of_favourite_club).update_column(:favourite, false)
assert_equal nil, Member.find(@member.id, :include => :favourite_club).reload.favourite_club
# conditions on the source table
assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :hairy_club).hairy_club
- clubs(:moustache_club).update_attribute(:name, "Association of Clean-Shaven Persons")
+ clubs(:moustache_club).update_column(:name, "Association of Clean-Shaven Persons")
assert_equal nil, Member.find(@member.id, :include => :hairy_club).reload.hairy_club
end
@@ -242,6 +245,25 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_has_one_through_polymorphic_with_primary_key_option
+ assert_equal categories(:general), authors(:david).essay_category
+
+ authors = Author.joins(:essay_category).where('categories.id' => categories(:general).id)
+ assert_equal authors(:david), authors.first
+
+ assert_equal owners(:blackbeard), authors(:david).essay_owner
+
+ authors = Author.joins(:essay_owner).where("owners.name = 'blackbeard'")
+ assert_equal authors(:david), authors.first
+ end
+
+ def test_has_one_through_with_primary_key_option
+ assert_equal categories(:general), authors(:david).essay_category_2
+
+ authors = Author.joins(:essay_category_2).where('categories.id' => categories(:general).id)
+ assert_equal authors(:david), authors.first
+ end
+
def test_has_one_through_with_default_scope_on_join_model
assert_equal posts(:welcome).comments.order('id').first, authors(:david).comment_on_first_post
end
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 19303fef9f..49a1c117bc 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -1,7 +1,9 @@
require "cases/helper"
+require 'active_support/core_ext/object/inclusion'
require 'models/tag'
require 'models/tagging'
require 'models/post'
+require 'models/rating'
require 'models/item'
require 'models/comment'
require 'models/author'
@@ -160,7 +162,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_many_with_delete_all
assert_equal 1, posts(:welcome).taggings.count
- posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDeleteAll'
+ posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyDeleteAll'
post = find_post_with_dependency(1, :has_many, :taggings, :delete_all)
old_count = Tagging.count
@@ -171,7 +173,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_many_with_destroy
assert_equal 1, posts(:welcome).taggings.count
- posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDestroy'
+ posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyDestroy'
post = find_post_with_dependency(1, :has_many, :taggings, :destroy)
old_count = Tagging.count
@@ -182,7 +184,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_many_with_nullify
assert_equal 1, posts(:welcome).taggings.count
- posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyNullify'
+ posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyNullify'
post = find_post_with_dependency(1, :has_many, :taggings, :nullify)
old_count = Tagging.count
@@ -193,7 +195,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_one_with_destroy
assert posts(:welcome).tagging
- posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneDestroy'
+ posts(:welcome).tagging.update_column :taggable_type, 'PostWithHasOneDestroy'
post = find_post_with_dependency(1, :has_one, :tagging, :destroy)
old_count = Tagging.count
@@ -204,7 +206,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_polymorphic_has_one_with_nullify
assert posts(:welcome).tagging
- posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneNullify'
+ posts(:welcome).tagging.update_column :taggable_type, 'PostWithHasOneNullify'
post = find_post_with_dependency(1, :has_one, :tagging, :nullify)
old_count = Tagging.count
@@ -288,7 +290,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_going_through_join_model_with_custom_foreign_key
- assert_equal [], posts(:thinking).authors
+ assert_equal [authors(:bob)], posts(:thinking).authors
assert_equal [authors(:mary)], posts(:authorless).authors
end
@@ -305,7 +307,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_with_custom_primary_key_on_has_many_source
- assert_equal [authors(:david)], posts(:thinking).authors_using_custom_pk
+ assert_equal [authors(:david), authors(:bob)], posts(:thinking).authors_using_custom_pk.order('authors.id')
end
def test_both_scoped_and_explicit_joins_should_be_respected
@@ -399,14 +401,6 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
end
- def test_has_many_through_has_many_through
- assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags }
- end
-
- def test_has_many_through_habtm
- assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories }
- end
-
def test_eager_load_has_many_through_has_many
author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id'
SpecialComment.new; VerySpecialComment.new
@@ -460,7 +454,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert saved_post.tags.include?(new_tag)
assert new_tag.persisted?
- assert saved_post.reload.tags(true).include?(new_tag)
+ assert new_tag.in?(saved_post.reload.tags(true))
new_post = Post.new(:title => "Association replacmenet works!", :body => "You best believe it.")
@@ -473,7 +467,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
new_post.save!
assert new_post.persisted?
- assert new_post.reload.tags(true).include?(saved_tag)
+ assert saved_tag.in?(new_post.reload.tags(true))
assert !posts(:thinking).tags.build.persisted?
assert !posts(:thinking).tags.new.persisted?
@@ -522,10 +516,10 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_collection_size_uses_counter_cache_if_it_exists
- author = authors(:david)
- author.stubs(:_read_attribute).with('comments_count').returns(100)
- assert_equal 100, author.comments.size
- assert !author.comments.loaded?
+ c = categories(:general)
+ c.categorizations_count = 100
+ assert_equal 100, c.categorizations.size
+ assert !c.categorizations.loaded?
end
def test_adding_junk_to_has_many_through_should_raise_type_mismatch
@@ -714,7 +708,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
# create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency)
class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}"
- Post.find(post_id).update_attribute :type, class_name
+ Post.find(post_id).update_column :type, class_name
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
klass.set_table_name 'posts'
klass.send(association, association_name, :as => :taggable, :dependent => dependency)
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
new file mode 100644
index 0000000000..dd450a2a8e
--- /dev/null
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -0,0 +1,546 @@
+require "cases/helper"
+require 'models/author'
+require 'models/post'
+require 'models/person'
+require 'models/reference'
+require 'models/job'
+require 'models/reader'
+require 'models/comment'
+require 'models/tag'
+require 'models/tagging'
+require 'models/subscriber'
+require 'models/book'
+require 'models/subscription'
+require 'models/rating'
+require 'models/member'
+require 'models/member_detail'
+require 'models/member_type'
+require 'models/sponsor'
+require 'models/club'
+require 'models/organization'
+require 'models/category'
+require 'models/categorization'
+require 'models/membership'
+require 'models/essay'
+
+class NestedThroughAssociationsTest < ActiveRecord::TestCase
+ fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings,
+ :people, :readers, :references, :jobs, :ratings, :comments, :members, :member_details,
+ :member_types, :sponsors, :clubs, :organizations, :categories, :categories_posts,
+ :categorizations, :memberships, :essays
+
+ # Through associations can either use the has_many or has_one macros.
+ #
+ # has_many
+ # - Source reflection can be has_many, has_one, belongs_to or has_and_belongs_to_many
+ # - Through reflection can be has_many, has_one, belongs_to or has_and_belongs_to_many
+ #
+ # has_one
+ # - Source reflection can be has_one or belongs_to
+ # - Through reflection can be has_one or belongs_to
+ #
+ # Additionally, the source reflection and/or through reflection may be subject to
+ # polymorphism and/or STI.
+ #
+ # When testing these, we need to make sure it works via loading the association directly, or
+ # joining the association, or including the association. We also need to ensure that associations
+ # are readonly where relevant.
+
+ # has_many through
+ # Source: has_many through
+ # Through: has_many
+ def test_has_many_through_has_many_with_has_many_through_source_reflection
+ general = tags(:general)
+ assert_equal [general, general], authors(:david).tags
+ end
+
+ def test_has_many_through_has_many_with_has_many_through_source_reflection_preload
+ authors = assert_queries(5) { Author.includes(:tags).to_a }
+ general = tags(:general)
+
+ assert_no_queries do
+ assert_equal [general, general], authors.first.tags
+ end
+ end
+
+ def test_has_many_through_has_many_with_has_many_through_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Author.where('tags.id' => tags(:general).id),
+ [authors(:david)], :tags
+ )
+
+ # This ensures that the polymorphism of taggings is being observed correctly
+ authors = Author.joins(:tags).where('taggings.taggable_type' => 'FakeModel')
+ assert authors.empty?
+ end
+
+ # has_many through
+ # Source: has_many
+ # Through: has_many through
+ def test_has_many_through_has_many_through_with_has_many_source_reflection
+ luke, david = subscribers(:first), subscribers(:second)
+ assert_equal [luke, david, david], authors(:david).subscribers.order('subscribers.nick')
+ end
+
+ def test_has_many_through_has_many_through_with_has_many_source_reflection_preload
+ luke, david = subscribers(:first), subscribers(:second)
+ authors = assert_queries(4) { Author.includes(:subscribers).to_a }
+ assert_no_queries do
+ assert_equal [luke, david, david], authors.first.subscribers.sort_by(&:nick)
+ end
+ end
+
+ def test_has_many_through_has_many_through_with_has_many_source_reflection_preload_via_joins
+ # All authors with subscribers where one of the subscribers' nick is 'alterself'
+ assert_includes_and_joins_equal(
+ Author.where('subscribers.nick' => 'alterself'),
+ [authors(:david)], :subscribers
+ )
+ end
+
+ # has_many through
+ # Source: has_one through
+ # Through: has_one
+ def test_has_many_through_has_one_with_has_one_through_source_reflection
+ assert_equal [member_types(:founding)], members(:groucho).nested_member_types
+ end
+
+ def test_has_many_through_has_one_with_has_one_through_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:nested_member_types).to_a }
+ founding = member_types(:founding)
+ assert_no_queries do
+ assert_equal [founding], members.first.nested_member_types
+ end
+ end
+
+ def test_has_many_through_has_one_with_has_one_through_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where('member_types.id' => member_types(:founding).id),
+ [members(:groucho)], :nested_member_types
+ )
+ end
+
+ # has_many through
+ # Source: has_one
+ # Through: has_one through
+ def test_has_many_through_has_one_through_with_has_one_source_reflection
+ assert_equal [sponsors(:moustache_club_sponsor_for_groucho)], members(:groucho).nested_sponsors
+ end
+
+ def test_has_many_through_has_one_through_with_has_one_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:nested_sponsors).to_a }
+ mustache = sponsors(:moustache_club_sponsor_for_groucho)
+ assert_no_queries do
+ assert_equal [mustache], members.first.nested_sponsors
+ end
+ end
+
+ def test_has_many_through_has_one_through_with_has_one_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where('sponsors.id' => sponsors(:moustache_club_sponsor_for_groucho).id),
+ [members(:groucho)], :nested_sponsors
+ )
+ end
+
+ # has_many through
+ # Source: has_many through
+ # Through: has_one
+ def test_has_many_through_has_one_with_has_many_through_source_reflection
+ groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
+
+ assert_equal [groucho_details, other_details],
+ members(:groucho).organization_member_details.order('member_details.id')
+ end
+
+ def test_has_many_through_has_one_with_has_many_through_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) }
+ groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
+
+ assert_no_queries do
+ assert_equal [groucho_details, other_details], members.first.organization_member_details.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_has_one_with_has_many_through_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'),
+ [members(:groucho), members(:some_other_guy)], :organization_member_details
+ )
+
+ members = Member.joins(:organization_member_details).
+ where('member_details.id' => 9)
+ assert members.empty?
+ end
+
+ # has_many through
+ # Source: has_many
+ # Through: has_one through
+ def test_has_many_through_has_one_through_with_has_many_source_reflection
+ groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
+
+ assert_equal [groucho_details, other_details],
+ members(:groucho).organization_member_details_2.order('member_details.id')
+ end
+
+ def test_has_many_through_has_one_through_with_has_many_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) }
+ groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
+
+ assert_no_queries do
+ assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_has_one_through_with_has_many_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where('member_details.id' => member_details(:groucho).id).order('member_details.id'),
+ [members(:groucho), members(:some_other_guy)], :organization_member_details_2
+ )
+
+ members = Member.joins(:organization_member_details_2).
+ where('member_details.id' => 9)
+ assert members.empty?
+ end
+
+ # has_many through
+ # Source: has_and_belongs_to_many
+ # Through: has_many
+ def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection
+ general, cooking = categories(:general), categories(:cooking)
+
+ assert_equal [general, cooking], authors(:bob).post_categories.order('categories.id')
+ end
+
+ def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload
+ authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) }
+ general, cooking = categories(:general), categories(:cooking)
+
+ assert_no_queries do
+ assert_equal [general, cooking], authors[2].post_categories.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Author.where('categories.id' => categories(:cooking).id),
+ [authors(:bob)], :post_categories
+ )
+ end
+
+ # has_many through
+ # Source: has_many
+ # Through: has_and_belongs_to_many
+ def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection
+ greetings, more = comments(:greetings), comments(:more_greetings)
+
+ assert_equal [greetings, more], categories(:technology).post_comments.order('comments.id')
+ end
+
+ def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload
+ categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) }
+ greetings, more = comments(:greetings), comments(:more_greetings)
+
+ assert_no_queries do
+ assert_equal [greetings, more], categories[1].post_comments.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Category.where('comments.id' => comments(:more_greetings).id).order('comments.id'),
+ [categories(:general), categories(:technology)], :post_comments
+ )
+ end
+
+ # has_many through
+ # Source: has_many through a habtm
+ # Through: has_many through
+ def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection
+ greetings, more = comments(:greetings), comments(:more_greetings)
+
+ assert_equal [greetings, more], authors(:bob).category_post_comments.order('comments.id')
+ end
+
+ def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload
+ authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) }
+ greetings, more = comments(:greetings), comments(:more_greetings)
+
+ assert_no_queries do
+ assert_equal [greetings, more], authors[2].category_post_comments.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Author.where('comments.id' => comments(:does_it_hurt).id).order('authors.id'),
+ [authors(:david), authors(:mary)], :category_post_comments
+ )
+ end
+
+ # has_many through
+ # Source: belongs_to
+ # Through: has_many through
+ def test_has_many_through_has_many_through_with_belongs_to_source_reflection
+ assert_equal [tags(:general), tags(:general)], authors(:david).tagging_tags
+ end
+
+ def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload
+ authors = assert_queries(5) { Author.includes(:tagging_tags).to_a }
+ general = tags(:general)
+
+ assert_no_queries do
+ assert_equal [general, general], authors.first.tagging_tags
+ end
+ end
+
+ def test_has_many_through_has_many_through_with_belongs_to_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Author.where('tags.id' => tags(:general).id),
+ [authors(:david)], :tagging_tags
+ )
+ end
+
+ # has_many through
+ # Source: has_many through
+ # Through: belongs_to
+ def test_has_many_through_belongs_to_with_has_many_through_source_reflection
+ welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general)
+
+ assert_equal [welcome_general, thinking_general],
+ categorizations(:david_welcome_general).post_taggings.order('taggings.id')
+ end
+
+ def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload
+ categorizations = assert_queries(4) { Categorization.includes(:post_taggings).to_a.sort_by(&:id) }
+ welcome_general, thinking_general = taggings(:welcome_general), taggings(:thinking_general)
+
+ assert_no_queries do
+ assert_equal [welcome_general, thinking_general], categorizations.first.post_taggings.sort_by(&:id)
+ end
+ end
+
+ def test_has_many_through_belongs_to_with_has_many_through_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Categorization.where('taggings.id' => taggings(:welcome_general).id).order('taggings.id'),
+ [categorizations(:david_welcome_general)], :post_taggings
+ )
+ end
+
+ # has_one through
+ # Source: has_one through
+ # Through: has_one
+ def test_has_one_through_has_one_with_has_one_through_source_reflection
+ assert_equal member_types(:founding), members(:groucho).nested_member_type
+ end
+
+ def test_has_one_through_has_one_with_has_one_through_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:nested_member_type).to_a.sort_by(&:id) }
+ founding = member_types(:founding)
+
+ assert_no_queries do
+ assert_equal founding, members.first.nested_member_type
+ end
+ end
+
+ def test_has_one_through_has_one_with_has_one_through_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where('member_types.id' => member_types(:founding).id),
+ [members(:groucho)], :nested_member_type
+ )
+ end
+
+ # has_one through
+ # Source: belongs_to
+ # Through: has_one through
+ def test_has_one_through_has_one_through_with_belongs_to_source_reflection
+ assert_equal categories(:general), members(:groucho).club_category
+ end
+
+ def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload
+ members = assert_queries(4) { Member.includes(:club_category).to_a.sort_by(&:id) }
+ general = categories(:general)
+
+ assert_no_queries do
+ assert_equal general, members.first.club_category
+ end
+ end
+
+ def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload_via_joins
+ assert_includes_and_joins_equal(
+ Member.where('categories.id' => categories(:technology).id),
+ [members(:blarpy_winkup)], :club_category
+ )
+ end
+
+ def test_distinct_has_many_through_a_has_many_through_association_on_source_reflection
+ author = authors(:david)
+ assert_equal [tags(:general)], author.distinct_tags
+ end
+
+ def test_distinct_has_many_through_a_has_many_through_association_on_through_reflection
+ author = authors(:david)
+ assert_equal [subscribers(:first), subscribers(:second)],
+ author.distinct_subscribers.order('subscribers.nick')
+ end
+
+ def test_nested_has_many_through_with_a_table_referenced_multiple_times
+ author = authors(:bob)
+ assert_equal [posts(:misc_by_bob), posts(:misc_by_mary), posts(:other_by_bob), posts(:other_by_mary)],
+ author.similar_posts.sort_by(&:id)
+
+ # Mary and Bob both have posts in misc, but they are the only ones.
+ authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id)
+ assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id)
+
+ # Check the polymorphism of taggings is being observed correctly (in both joins)
+ authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel')
+ assert authors.empty?
+ authors = Author.joins(:similar_posts).where('taggings_authors_join.taggable_type' => 'FakeModel')
+ assert authors.empty?
+ end
+
+ def test_has_many_through_with_foreign_key_option_on_through_reflection
+ assert_equal [posts(:welcome), posts(:authorless)], people(:david).agents_posts.order('posts.id')
+ assert_equal [authors(:david)], references(:david_unicyclist).agents_posts_authors
+
+ references = Reference.joins(:agents_posts_authors).where('authors.id' => authors(:david).id)
+ assert_equal [references(:david_unicyclist)], references
+ end
+
+ def test_has_many_through_with_foreign_key_option_on_source_reflection
+ assert_equal [people(:michael), people(:susan)], jobs(:unicyclist).agents.order('people.id')
+
+ jobs = Job.joins(:agents)
+ assert_equal [jobs(:unicyclist), jobs(:unicyclist)], jobs
+ end
+
+ def test_has_many_through_with_sti_on_through_reflection
+ ratings = posts(:sti_comments).special_comments_ratings.sort_by(&:id)
+ assert_equal [ratings(:special_comment_rating), ratings(:sub_special_comment_rating)], ratings
+
+ # Ensure STI is respected in the join
+ scope = Post.joins(:special_comments_ratings).where(:id => posts(:sti_comments).id)
+ assert scope.where("comments.type" => "Comment").empty?
+ assert !scope.where("comments.type" => "SpecialComment").empty?
+ assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ end
+
+ def test_has_many_through_with_sti_on_nested_through_reflection
+ taggings = posts(:sti_comments).special_comments_ratings_taggings
+ assert_equal [taggings(:special_comment_rating)], taggings
+
+ scope = Post.joins(:special_comments_ratings_taggings).where(:id => posts(:sti_comments).id)
+ assert scope.where("comments.type" => "Comment").empty?
+ assert !scope.where("comments.type" => "SpecialComment").empty?
+ end
+
+ def test_nested_has_many_through_writers_should_raise_error
+ david = authors(:david)
+ subscriber = subscribers(:first)
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers = [subscriber]
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscriber_ids = [subscriber.id]
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers << subscriber
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers.delete(subscriber)
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers.clear
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers.build
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers.create
+ end
+ end
+
+ def test_nested_has_one_through_writers_should_raise_error
+ groucho = members(:groucho)
+ founding = member_types(:founding)
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ groucho.nested_member_type = founding
+ end
+ end
+
+ def test_nested_has_many_through_with_conditions_on_through_associations
+ assert_equal [tags(:blue)], authors(:bob).misc_post_first_blue_tags
+ end
+
+ def test_nested_has_many_through_with_conditions_on_through_associations_preload
+ assert Author.where('tags.id' => 100).joins(:misc_post_first_blue_tags).empty?
+
+ authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) }
+ blue = tags(:blue)
+
+ assert_no_queries do
+ assert_equal [blue], authors[2].misc_post_first_blue_tags
+ end
+ end
+
+ def test_nested_has_many_through_with_conditions_on_through_associations_preload_via_joins
+ # Pointless condition to force single-query loading
+ assert_includes_and_joins_equal(
+ Author.where('tags.id = tags.id'),
+ [authors(:bob)], :misc_post_first_blue_tags
+ )
+ end
+
+ def test_nested_has_many_through_with_conditions_on_source_associations
+ assert_equal [tags(:blue)], authors(:bob).misc_post_first_blue_tags_2
+ end
+
+ def test_nested_has_many_through_with_conditions_on_source_associations_preload
+ authors = assert_queries(4) { Author.includes(:misc_post_first_blue_tags_2).to_a.sort_by(&:id) }
+ blue = tags(:blue)
+
+ assert_no_queries do
+ assert_equal [blue], authors[2].misc_post_first_blue_tags_2
+ end
+ end
+
+ def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins
+ # Pointless condition to force single-query loading
+ assert_includes_and_joins_equal(
+ Author.where('tags.id = tags.id'),
+ [authors(:bob)], :misc_post_first_blue_tags_2
+ )
+ end
+
+ def test_nested_has_many_through_with_foreign_key_option_on_the_source_reflection_through_reflection
+ assert_equal [categories(:general)], organizations(:nsa).author_essay_categories
+
+ organizations = Organization.joins(:author_essay_categories).
+ where('categories.id' => categories(:general).id)
+ assert_equal [organizations(:nsa)], organizations
+
+ assert_equal categories(:general), organizations(:nsa).author_owned_essay_category
+
+ organizations = Organization.joins(:author_owned_essay_category).
+ where('categories.id' => categories(:general).id)
+ assert_equal [organizations(:nsa)], organizations
+ end
+
+ private
+
+ def assert_includes_and_joins_equal(query, expected, association)
+ actual = assert_queries(1) { query.joins(association).to_a.uniq }
+ assert_equal expected, actual
+
+ actual = assert_queries(1) { query.includes(association).to_a.uniq }
+ assert_equal expected, actual
+ end
+end