1492 lines
44 KiB
Ruby
1492 lines
44 KiB
Ruby
require 'active_support/core_ext/module/delegation'
|
|
|
|
module Shoulda
|
|
module Matchers
|
|
module ActiveRecord
|
|
# The `belong_to` matcher is used to ensure that a `belong_to` association
|
|
# exists on your model.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :organization
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should belong_to(:organization) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:organization)
|
|
# end
|
|
#
|
|
# Note that polymorphic associations are automatically detected and do not
|
|
# need any qualifiers:
|
|
#
|
|
# class Comment < ActiveRecord::Base
|
|
# belongs_to :commentable, polymorphic: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Comment, type: :model do
|
|
# it { should belong_to(:commentable) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class CommentTest < ActiveSupport::TestCase
|
|
# should belong_to(:commentable)
|
|
# end
|
|
#
|
|
# #### Qualifiers
|
|
#
|
|
# ##### conditions
|
|
#
|
|
# Use `conditions` if your association is defined with a scope that sets
|
|
# the `where` clause.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :family, -> { where(everyone_is_perfect: false) }
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it do
|
|
# should belong_to(:family).
|
|
# conditions(everyone_is_perfect: false)
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:family).
|
|
# conditions(everyone_is_perfect: false)
|
|
# end
|
|
#
|
|
# ##### order
|
|
#
|
|
# Use `order` if your association is defined with a scope that sets the
|
|
# `order` clause.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :previous_company, -> { order('hired_on desc') }
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should belong_to(:previous_company).order('hired_on desc') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:previous_company).order('hired_on desc')
|
|
# end
|
|
#
|
|
# ##### class_name
|
|
#
|
|
# Use `class_name` to test usage of the `:class_name` option. This
|
|
# asserts that the model you're referring to actually exists.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :ancient_city, class_name: 'City'
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should belong_to(:ancient_city).class_name('City') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:ancient_city).class_name('City')
|
|
# end
|
|
#
|
|
# ##### with_primary_key
|
|
#
|
|
# Use `with_primary_key` to test usage of the `:primary_key` option.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :great_country, primary_key: 'country_id'
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it do
|
|
# should belong_to(:great_country).
|
|
# with_primary_key('country_id')
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:great_country).
|
|
# with_primary_key('country_id')
|
|
# end
|
|
#
|
|
# ##### with_foreign_key
|
|
#
|
|
# Use `with_foreign_key` to test usage of the `:foreign_key` option.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :great_country, foreign_key: 'country_id'
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it do
|
|
# should belong_to(:great_country).
|
|
# with_foreign_key('country_id')
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:great_country).
|
|
# with_foreign_key('country_id')
|
|
# end
|
|
#
|
|
# ##### dependent
|
|
#
|
|
# Use `dependent` to assert that the `:dependent` option was specified.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :world, dependent: :destroy
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should belong_to(:world).dependent(:destroy) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:world).dependent(:destroy)
|
|
# end
|
|
#
|
|
# To assert that *any* `:dependent` option was specified, use `true`:
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should belong_to(:world).dependent(true) }
|
|
# end
|
|
#
|
|
# To assert that *no* `:dependent` option was specified, use `false`:
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :company
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should belong_to(:company).dependent(false) }
|
|
# end
|
|
#
|
|
# ##### counter_cache
|
|
#
|
|
# Use `counter_cache` to assert that the `:counter_cache` option was
|
|
# specified.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :organization, counter_cache: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should belong_to(:organization).counter_cache(true) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:organization).counter_cache(true)
|
|
# end
|
|
#
|
|
# ##### touch
|
|
#
|
|
# Use `touch` to assert that the `:touch` option was specified.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :organization, touch: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should belong_to(:organization).touch(true) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:organization).touch(true)
|
|
# end
|
|
#
|
|
# ##### autosave
|
|
#
|
|
# Use `autosave` to assert that the `:autosave` option was specified.
|
|
#
|
|
# class Account < ActiveRecord::Base
|
|
# belongs_to :bank, autosave: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Account, type: :model do
|
|
# it { should belong_to(:bank).autosave(true) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class AccountTest < ActiveSupport::TestCase
|
|
# should belong_to(:bank).autosave(true)
|
|
# end
|
|
#
|
|
# ##### inverse_of
|
|
#
|
|
# Use `inverse_of` to assert that the `:inverse_of` option was specified.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :organization, inverse_of: :employees
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# describe Person
|
|
# it { should belong_to(:organization).inverse_of(:employees) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:organization).inverse_of(:employees)
|
|
# end
|
|
#
|
|
# ##### required
|
|
#
|
|
# Use `required` to assert that the association is not allowed to be nil.
|
|
# (Enabled by default in Rails 5+.)
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :organization, required: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# describe Person
|
|
# it { should belong_to(:organization).required }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:organization).required
|
|
# end
|
|
#
|
|
# ##### without_validating_presence
|
|
#
|
|
# Use `without_validating_presence` with `belong_to` to prevent the
|
|
# matcher from checking whether the association disallows nil (Rails 5+
|
|
# only). This can be helpful if you have a custom hook that always sets
|
|
# the association to a meaningful value:
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :organization
|
|
#
|
|
# before_validation :autoassign_organization
|
|
#
|
|
# private
|
|
#
|
|
# def autoassign_organization
|
|
# self.organization = Organization.create!
|
|
# end
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# describe Person
|
|
# it { should belong_to(:organization).without_validating_presence }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:organization).without_validating_presence
|
|
# end
|
|
#
|
|
# ##### optional
|
|
#
|
|
# Use `optional` to assert that the association is allowed to be nil.
|
|
# (Rails 5+ only.)
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# belongs_to :organization, optional: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# describe Person
|
|
# it { should belong_to(:organization).optional }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should belong_to(:organization).optional
|
|
# end
|
|
#
|
|
# @return [AssociationMatcher]
|
|
#
|
|
def belong_to(name)
|
|
AssociationMatcher.new(:belongs_to, name)
|
|
end
|
|
|
|
# The `have_many` matcher is used to test that a `has_many` or `has_many
|
|
# :through` association exists on your model.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :friends
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_many(:friends) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:friends)
|
|
# end
|
|
#
|
|
# Note that polymorphic associations are automatically detected and do not
|
|
# need any qualifiers:
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :pictures, as: :imageable
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_many(:pictures) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:pictures)
|
|
# end
|
|
#
|
|
# #### Qualifiers
|
|
#
|
|
# ##### conditions
|
|
#
|
|
# Use `conditions` if your association is defined with a scope that sets
|
|
# the `where` clause.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :coins, -> { where(quality: 'mint') }
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_many(:coins).conditions(quality: 'mint') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:coins).conditions(quality: 'mint')
|
|
# end
|
|
#
|
|
# ##### order
|
|
#
|
|
# Use `order` if your association is defined with a scope that sets the
|
|
# `order` clause.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :shirts, -> { order('color') }
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_many(:shirts).order('color') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:shirts).order('color')
|
|
# end
|
|
#
|
|
# ##### class_name
|
|
#
|
|
# Use `class_name` to test usage of the `:class_name` option. This
|
|
# asserts that the model you're referring to actually exists.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :hopes, class_name: 'Dream'
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_many(:hopes).class_name('Dream') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:hopes).class_name('Dream')
|
|
# end
|
|
#
|
|
# ##### with_primary_key
|
|
#
|
|
# Use `with_primary_key` to test usage of the `:primary_key` option.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :worries, primary_key: 'worrier_id'
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_many(:worries).with_primary_key('worrier_id') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:worries).with_primary_key('worrier_id')
|
|
# end
|
|
#
|
|
# ##### with_foreign_key
|
|
#
|
|
# Use `with_foreign_key` to test usage of the `:foreign_key` option.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :worries, foreign_key: 'worrier_id'
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_many(:worries).with_foreign_key('worrier_id') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:worries).with_foreign_key('worrier_id')
|
|
# end
|
|
#
|
|
# ##### dependent
|
|
#
|
|
# Use `dependent` to assert that the `:dependent` option was specified.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :secret_documents, dependent: :destroy
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_many(:secret_documents).dependent(:destroy) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:secret_documents).dependent(:destroy)
|
|
# end
|
|
#
|
|
# ##### through
|
|
#
|
|
# Use `through` to test usage of the `:through` option. This asserts that
|
|
# the association you are going through actually exists.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :acquaintances, through: :friends
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_many(:acquaintances).through(:friends) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:acquaintances).through(:friends)
|
|
# end
|
|
#
|
|
# ##### source
|
|
#
|
|
# Use `source` to test usage of the `:source` option on a `:through`
|
|
# association.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :job_offers, through: :friends, source: :opportunities
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it do
|
|
# should have_many(:job_offers).
|
|
# through(:friends).
|
|
# source(:opportunities)
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:job_offers).
|
|
# through(:friends).
|
|
# source(:opportunities)
|
|
# end
|
|
#
|
|
# ##### validate
|
|
#
|
|
# Use `validate` to assert that the `:validate` option was specified.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_many :ideas, validate: false
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_many(:ideas).validate(false) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_many(:ideas).validate(false)
|
|
# end
|
|
#
|
|
# ##### autosave
|
|
#
|
|
# Use `autosave` to assert that the `:autosave` option was specified.
|
|
#
|
|
# class Player < ActiveRecord::Base
|
|
# has_many :games, autosave: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Player, type: :model do
|
|
# it { should have_many(:games).autosave(true) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PlayerTest < ActiveSupport::TestCase
|
|
# should have_many(:games).autosave(true)
|
|
# end
|
|
#
|
|
# ##### index_errors
|
|
#
|
|
# Use `index_errors` to assert that the `:index_errors` option was
|
|
# specified.
|
|
#
|
|
# class Player < ActiveRecord::Base
|
|
# has_many :games, index_errors: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Player, type: :model do
|
|
# it { should have_many(:games).index_errors(true) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PlayerTest < ActiveSupport::TestCase
|
|
# should have_many(:games).index_errors(true)
|
|
# end
|
|
#
|
|
# ##### inverse_of
|
|
#
|
|
# Use `inverse_of` to assert that the `:inverse_of` option was specified.
|
|
#
|
|
# class Organization < ActiveRecord::Base
|
|
# has_many :employees, inverse_of: :company
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# describe Organization
|
|
# it { should have_many(:employees).inverse_of(:company) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class OrganizationTest < ActiveSupport::TestCase
|
|
# should have_many(:employees).inverse_of(:company)
|
|
# end
|
|
#
|
|
# @return [AssociationMatcher]
|
|
#
|
|
def have_many(name)
|
|
AssociationMatcher.new(:has_many, name)
|
|
end
|
|
|
|
# The `have_one` matcher is used to test that a `has_one` or `has_one
|
|
# :through` association exists on your model.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :partner
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_one(:partner) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:partner)
|
|
# end
|
|
#
|
|
# #### Qualifiers
|
|
#
|
|
# ##### conditions
|
|
#
|
|
# Use `conditions` if your association is defined with a scope that sets
|
|
# the `where` clause.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :pet, -> { where('weight < 80') }
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_one(:pet).conditions('weight < 80') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:pet).conditions('weight < 80')
|
|
# end
|
|
#
|
|
# ##### order
|
|
#
|
|
# Use `order` if your association is defined with a scope that sets the
|
|
# `order` clause.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :focus, -> { order('priority desc') }
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_one(:focus).order('priority desc') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:focus).order('priority desc')
|
|
# end
|
|
#
|
|
# ##### class_name
|
|
#
|
|
# Use `class_name` to test usage of the `:class_name` option. This
|
|
# asserts that the model you're referring to actually exists.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :chance, class_name: 'Opportunity'
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_one(:chance).class_name('Opportunity') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:chance).class_name('Opportunity')
|
|
# end
|
|
#
|
|
# ##### dependent
|
|
#
|
|
# Use `dependent` to test that the `:dependent` option was specified.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :contract, dependent: :nullify
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_one(:contract).dependent(:nullify) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:contract).dependent(:nullify)
|
|
# end
|
|
#
|
|
# ##### with_primary_key
|
|
#
|
|
# Use `with_primary_key` to test usage of the `:primary_key` option.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :job, primary_key: 'worker_id'
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_one(:job).with_primary_key('worker_id') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:job).with_primary_key('worker_id')
|
|
# end
|
|
#
|
|
# ##### with_foreign_key
|
|
#
|
|
# Use `with_foreign_key` to test usage of the `:foreign_key` option.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :job, foreign_key: 'worker_id'
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_one(:job).with_foreign_key('worker_id') }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:job).with_foreign_key('worker_id')
|
|
# end
|
|
#
|
|
# ##### through
|
|
#
|
|
# Use `through` to test usage of the `:through` option. This asserts that
|
|
# the association you are going through actually exists.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :life, through: :partner
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_one(:life).through(:partner) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:life).through(:partner)
|
|
# end
|
|
#
|
|
# ##### source
|
|
#
|
|
# Use `source` to test usage of the `:source` option on a `:through`
|
|
# association.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :car, through: :partner, source: :vehicle
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_one(:car).through(:partner).source(:vehicle) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:car).through(:partner).source(:vehicle)
|
|
# end
|
|
#
|
|
# ##### validate
|
|
#
|
|
# Use `validate` to assert that the the `:validate` option was specified.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :parking_card, validate: false
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_one(:parking_card).validate(false) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:parking_card).validate(false)
|
|
# end
|
|
#
|
|
# ##### autosave
|
|
#
|
|
# Use `autosave` to assert that the `:autosave` option was specified.
|
|
#
|
|
# class Account < ActiveRecord::Base
|
|
# has_one :bank, autosave: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Account, type: :model do
|
|
# it { should have_one(:bank).autosave(true) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class AccountTest < ActiveSupport::TestCase
|
|
# should have_one(:bank).autosave(true)
|
|
# end
|
|
#
|
|
# ##### required
|
|
#
|
|
# Use `required` to assert that the association is not allowed to be nil.
|
|
# (Rails 5+ only.)
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_one :brain, required: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# describe Person
|
|
# it { should have_one(:brain).required }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_one(:brain).required
|
|
# end
|
|
#
|
|
# @return [AssociationMatcher]
|
|
#
|
|
def have_one(name)
|
|
AssociationMatcher.new(:has_one, name)
|
|
end
|
|
|
|
# The `have_and_belong_to_many` matcher is used to test that a
|
|
# `has_and_belongs_to_many` association exists on your model and that the
|
|
# join table exists in the database.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_and_belongs_to_many :awards
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it { should have_and_belong_to_many(:awards) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_and_belong_to_many(:awards)
|
|
# end
|
|
#
|
|
# #### Qualifiers
|
|
#
|
|
# ##### conditions
|
|
#
|
|
# Use `conditions` if your association is defined with a scope that sets
|
|
# the `where` clause.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_and_belongs_to_many :issues, -> { where(difficulty: 'hard') }
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it do
|
|
# should have_and_belong_to_many(:issues).
|
|
# conditions(difficulty: 'hard')
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_and_belong_to_many(:issues).
|
|
# conditions(difficulty: 'hard')
|
|
# end
|
|
#
|
|
# ##### order
|
|
#
|
|
# Use `order` if your association is defined with a scope that sets the
|
|
# `order` clause.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_and_belongs_to_many :projects, -> { order('time_spent') }
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it do
|
|
# should have_and_belong_to_many(:projects).
|
|
# order('time_spent')
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_and_belong_to_many(:projects).
|
|
# order('time_spent')
|
|
# end
|
|
#
|
|
# ##### class_name
|
|
#
|
|
# Use `class_name` to test usage of the `:class_name` option. This
|
|
# asserts that the model you're referring to actually exists.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_and_belongs_to_many :places_visited, class_name: 'City'
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it do
|
|
# should have_and_belong_to_many(:places_visited).
|
|
# class_name('City')
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_and_belong_to_many(:places_visited).
|
|
# class_name('City')
|
|
# end
|
|
#
|
|
# ##### join_table
|
|
#
|
|
# Use `join_table` to test usage of the `:join_table` option. This
|
|
# asserts that the table you're referring to actually exists.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_and_belongs_to_many :issues, join_table: :people_tickets
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it do
|
|
# should have_and_belong_to_many(:issues).
|
|
# join_table(:people_tickets)
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_and_belong_to_many(:issues).
|
|
# join_table(:people_tickets)
|
|
# end
|
|
#
|
|
# ##### validate
|
|
#
|
|
# Use `validate` to test that the `:validate` option was specified.
|
|
#
|
|
# class Person < ActiveRecord::Base
|
|
# has_and_belongs_to_many :interviews, validate: false
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Person, type: :model do
|
|
# it do
|
|
# should have_and_belong_to_many(:interviews).
|
|
# validate(false)
|
|
# end
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class PersonTest < ActiveSupport::TestCase
|
|
# should have_and_belong_to_many(:interviews).
|
|
# validate(false)
|
|
# end
|
|
#
|
|
# ##### autosave
|
|
#
|
|
# Use `autosave` to assert that the `:autosave` option was specified.
|
|
#
|
|
# class Publisher < ActiveRecord::Base
|
|
# has_and_belongs_to_many :advertisers, autosave: true
|
|
# end
|
|
#
|
|
# # RSpec
|
|
# RSpec.describe Publisher, type: :model do
|
|
# it { should have_and_belong_to_many(:advertisers).autosave(true) }
|
|
# end
|
|
#
|
|
# # Minitest (Shoulda)
|
|
# class AccountTest < ActiveSupport::TestCase
|
|
# should have_and_belong_to_many(:advertisers).autosave(true)
|
|
# end
|
|
#
|
|
# @return [AssociationMatcher]
|
|
#
|
|
def have_and_belong_to_many(name)
|
|
AssociationMatcher.new(:has_and_belongs_to_many, name)
|
|
end
|
|
|
|
# @private
|
|
class AssociationMatcher
|
|
MACROS = {
|
|
'belongs_to' => 'belong to',
|
|
'has_many' => 'have many',
|
|
'has_one' => 'have one',
|
|
'has_and_belongs_to_many' => 'have and belong to many',
|
|
}.freeze
|
|
|
|
delegate :reflection, :model_class, :associated_class, :through?,
|
|
:polymorphic?, to: :reflector
|
|
|
|
attr_reader :name, :options
|
|
|
|
def initialize(macro, name)
|
|
@macro = macro
|
|
@name = name
|
|
@options = {}
|
|
@submatchers = []
|
|
@missing = ''
|
|
|
|
if macro == :belongs_to
|
|
required(belongs_to_required_by_default?)
|
|
end
|
|
end
|
|
|
|
def through(through)
|
|
add_submatcher(
|
|
AssociationMatchers::ThroughMatcher,
|
|
through,
|
|
name,
|
|
)
|
|
self
|
|
end
|
|
|
|
def dependent(dependent)
|
|
add_submatcher(
|
|
AssociationMatchers::DependentMatcher,
|
|
dependent,
|
|
name,
|
|
)
|
|
self
|
|
end
|
|
|
|
def order(order)
|
|
add_submatcher(
|
|
AssociationMatchers::OrderMatcher,
|
|
order,
|
|
name,
|
|
)
|
|
self
|
|
end
|
|
|
|
def counter_cache(counter_cache = true)
|
|
add_submatcher(
|
|
AssociationMatchers::CounterCacheMatcher,
|
|
counter_cache,
|
|
name,
|
|
)
|
|
self
|
|
end
|
|
|
|
def inverse_of(inverse_of)
|
|
add_submatcher(
|
|
AssociationMatchers::InverseOfMatcher,
|
|
inverse_of,
|
|
name,
|
|
)
|
|
self
|
|
end
|
|
|
|
def source(source)
|
|
add_submatcher(
|
|
AssociationMatchers::SourceMatcher,
|
|
source,
|
|
name,
|
|
)
|
|
self
|
|
end
|
|
|
|
def conditions(conditions)
|
|
@options[:conditions] = conditions
|
|
self
|
|
end
|
|
|
|
def autosave(autosave)
|
|
@options[:autosave] = autosave
|
|
self
|
|
end
|
|
|
|
def index_errors(index_errors)
|
|
@options[:index_errors] = index_errors
|
|
self
|
|
end
|
|
|
|
def class_name(class_name)
|
|
@options[:class_name] = class_name
|
|
self
|
|
end
|
|
|
|
def with_foreign_key(foreign_key)
|
|
@options[:foreign_key] = foreign_key
|
|
self
|
|
end
|
|
|
|
def with_primary_key(primary_key)
|
|
@options[:primary_key] = primary_key
|
|
self
|
|
end
|
|
|
|
def required(required = true)
|
|
remove_submatcher(AssociationMatchers::OptionalMatcher)
|
|
add_submatcher(
|
|
AssociationMatchers::RequiredMatcher,
|
|
name,
|
|
required,
|
|
)
|
|
self
|
|
end
|
|
|
|
def optional(optional = true)
|
|
remove_submatcher(AssociationMatchers::RequiredMatcher)
|
|
add_submatcher(
|
|
AssociationMatchers::OptionalMatcher,
|
|
name,
|
|
optional,
|
|
)
|
|
self
|
|
end
|
|
|
|
def validate(validate = true)
|
|
@options[:validate] = validate
|
|
self
|
|
end
|
|
|
|
def touch(touch = true)
|
|
@options[:touch] = touch
|
|
self
|
|
end
|
|
|
|
def join_table(join_table_name)
|
|
@options[:join_table_name] = join_table_name
|
|
self
|
|
end
|
|
|
|
def without_validating_presence
|
|
remove_submatcher(AssociationMatchers::RequiredMatcher)
|
|
self
|
|
end
|
|
|
|
def description
|
|
description = "#{macro_description} #{name}"
|
|
if options.key?(:class_name)
|
|
description += " class_name => #{options[:class_name]}"
|
|
end
|
|
[description, submatchers.map(&:description)].flatten.join(' ')
|
|
end
|
|
|
|
def failure_message
|
|
"Expected #{expectation} (#{missing_options})"
|
|
end
|
|
|
|
def failure_message_when_negated
|
|
"Did not expect #{expectation}"
|
|
end
|
|
|
|
def matches?(subject)
|
|
@subject = subject
|
|
association_exists? &&
|
|
macro_correct? &&
|
|
validate_inverse_of_through_association &&
|
|
(polymorphic? || class_exists?) &&
|
|
foreign_key_exists? &&
|
|
primary_key_exists? &&
|
|
class_name_correct? &&
|
|
join_table_correct? &&
|
|
autosave_correct? &&
|
|
index_errors_correct? &&
|
|
conditions_correct? &&
|
|
validate_correct? &&
|
|
touch_correct? &&
|
|
submatchers_match?
|
|
end
|
|
|
|
def join_table_name
|
|
options[:join_table_name] || reflector.join_table_name
|
|
end
|
|
|
|
def option_verifier
|
|
@_option_verifier ||=
|
|
AssociationMatchers::OptionVerifier.new(reflector)
|
|
end
|
|
|
|
protected
|
|
|
|
attr_reader :submatchers, :missing, :subject, :macro
|
|
|
|
def reflector
|
|
@_reflector ||= AssociationMatchers::ModelReflector.new(subject, name)
|
|
end
|
|
|
|
def add_submatcher(matcher_class, *args)
|
|
remove_submatcher(matcher_class)
|
|
submatchers << matcher_class.new(*args)
|
|
end
|
|
|
|
def remove_submatcher(matcher_class)
|
|
submatchers.delete_if do |submatcher|
|
|
submatcher.is_a?(matcher_class)
|
|
end
|
|
end
|
|
|
|
def macro_description
|
|
MACROS[macro.to_s]
|
|
end
|
|
|
|
def expectation
|
|
expectation =
|
|
"#{model_class.name} to have a #{macro} association called #{name}"
|
|
|
|
if through?
|
|
expectation << " through #{reflector.has_and_belongs_to_many_name}"
|
|
end
|
|
|
|
expectation
|
|
end
|
|
|
|
def missing_options
|
|
missing_options = [missing, missing_options_for_failing_submatchers]
|
|
missing_options.flatten.select(&:present?).join(', ')
|
|
end
|
|
|
|
def failing_submatchers
|
|
@_failing_submatchers ||= submatchers.reject do |matcher|
|
|
matcher.matches?(subject)
|
|
end
|
|
end
|
|
|
|
def missing_options_for_failing_submatchers
|
|
if defined?(@_failing_submatchers)
|
|
@_failing_submatchers.map(&:missing_option)
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def association_exists?
|
|
if reflection.nil?
|
|
@missing = "no association called #{name}"
|
|
false
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
def macro_correct?
|
|
if reflection.macro == macro
|
|
true
|
|
elsif reflection.macro == :has_many
|
|
macro == :has_and_belongs_to_many &&
|
|
reflection.name == @name
|
|
else
|
|
@missing = "actual association type was #{reflection.macro}"
|
|
false
|
|
end
|
|
end
|
|
|
|
def validate_inverse_of_through_association
|
|
reflector.validate_inverse_of_through_association!
|
|
true
|
|
rescue ::ActiveRecord::ActiveRecordError => e
|
|
@missing = e.message
|
|
false
|
|
end
|
|
|
|
def macro_supports_primary_key?
|
|
macro == :belongs_to ||
|
|
([:has_many, :has_one].include?(macro) && !through?)
|
|
end
|
|
|
|
def foreign_key_exists?
|
|
!(belongs_foreign_key_missing? || has_foreign_key_missing?)
|
|
end
|
|
|
|
def primary_key_exists?
|
|
!macro_supports_primary_key? || primary_key_correct?(model_class)
|
|
end
|
|
|
|
def belongs_foreign_key_missing?
|
|
macro == :belongs_to && !class_has_foreign_key?(model_class)
|
|
end
|
|
|
|
def has_foreign_key_missing?
|
|
[:has_many, :has_one].include?(macro) &&
|
|
!through? &&
|
|
!class_has_foreign_key?(associated_class)
|
|
end
|
|
|
|
def class_name_correct?
|
|
if options.key?(:class_name)
|
|
if option_verifier.correct_for_constant?(
|
|
:class_name,
|
|
options[:class_name],
|
|
)
|
|
true
|
|
else
|
|
@missing = "#{name} should resolve to #{options[:class_name]}"\
|
|
' for class_name'
|
|
false
|
|
end
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
def join_table_correct?
|
|
if (
|
|
macro != :has_and_belongs_to_many ||
|
|
join_table_matcher.matches?(@subject)
|
|
)
|
|
true
|
|
else
|
|
@missing = join_table_matcher.failure_message
|
|
false
|
|
end
|
|
end
|
|
|
|
def join_table_matcher
|
|
@_join_table_matcher ||= AssociationMatchers::JoinTableMatcher.new(
|
|
self,
|
|
reflector,
|
|
)
|
|
end
|
|
|
|
def class_exists?
|
|
associated_class
|
|
true
|
|
rescue NameError
|
|
@missing = "#{reflection.class_name} does not exist"
|
|
false
|
|
end
|
|
|
|
def autosave_correct?
|
|
if options.key?(:autosave)
|
|
if option_verifier.correct_for_boolean?(
|
|
:autosave,
|
|
options[:autosave],
|
|
)
|
|
true
|
|
else
|
|
@missing = "#{name} should have autosave set to"\
|
|
" #{options[:autosave]}"
|
|
false
|
|
end
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
def index_errors_correct?
|
|
return true unless options.key?(:index_errors)
|
|
|
|
if option_verifier.correct_for_boolean?(
|
|
:index_errors,
|
|
options[:index_errors],
|
|
)
|
|
true
|
|
else
|
|
@missing =
|
|
"#{name} should have index_errors set to " +
|
|
options[:index_errors].to_s
|
|
false
|
|
end
|
|
end
|
|
|
|
def conditions_correct?
|
|
if options.key?(:conditions)
|
|
if option_verifier.correct_for_relation_clause?(
|
|
:conditions,
|
|
options[:conditions],
|
|
)
|
|
true
|
|
else
|
|
@missing = "#{name} should have the following conditions:" +
|
|
" #{options[:conditions]}"
|
|
false
|
|
end
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
def validate_correct?
|
|
if option_verifier.correct_for_boolean?(:validate, options[:validate])
|
|
true
|
|
else
|
|
@missing = "#{name} should have validate: #{options[:validate]}"
|
|
false
|
|
end
|
|
end
|
|
|
|
def touch_correct?
|
|
if option_verifier.correct_for_boolean?(:touch, options[:touch])
|
|
true
|
|
else
|
|
@missing = "#{name} should have touch: #{options[:touch]}"
|
|
false
|
|
end
|
|
end
|
|
|
|
def class_has_foreign_key?(klass)
|
|
if options.key?(:foreign_key) && !foreign_key_correct?
|
|
@missing = foreign_key_failure_message(klass, options[:foreign_key])
|
|
false
|
|
elsif !has_column?(klass, foreign_key)
|
|
@missing = foreign_key_failure_message(klass, foreign_key)
|
|
false
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
def has_column?(klass, column)
|
|
case column
|
|
when Array
|
|
column.all? { |c| has_column?(klass, c) }
|
|
else
|
|
column_names_for(klass).include?(column)
|
|
end
|
|
end
|
|
|
|
def foreign_key_correct?
|
|
option_verifier.correct_for_string?(
|
|
:foreign_key,
|
|
options[:foreign_key],
|
|
)
|
|
end
|
|
|
|
def foreign_key_failure_message(klass, foreign_key)
|
|
"#{klass} does not have a #{foreign_key} foreign key."
|
|
end
|
|
|
|
def primary_key_correct?(klass)
|
|
if options.key?(:primary_key)
|
|
if option_verifier.correct_for_string?(
|
|
:primary_key,
|
|
options[:primary_key],
|
|
)
|
|
true
|
|
else
|
|
@missing = "#{klass} does not have a #{options[:primary_key]}"\
|
|
' primary key'
|
|
false
|
|
end
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
def foreign_key
|
|
key = if foreign_key_reflection
|
|
if foreign_key_reflection.respond_to?(:foreign_key)
|
|
foreign_key_reflection.foreign_key
|
|
else
|
|
foreign_key_reflection.primary_key_name
|
|
end
|
|
end
|
|
|
|
if key.is_a?(Array)
|
|
key.map(&:to_s)
|
|
else
|
|
key.to_s
|
|
end
|
|
end
|
|
|
|
def foreign_key_reflection
|
|
if (
|
|
[:has_one, :has_many].include?(macro) &&
|
|
reflection.options.include?(:inverse_of) &&
|
|
reflection.options[:inverse_of] != false
|
|
)
|
|
associated_class.reflect_on_association(
|
|
reflection.options[:inverse_of],
|
|
)
|
|
else
|
|
reflection
|
|
end
|
|
end
|
|
|
|
def submatchers_match?
|
|
failing_submatchers.empty?
|
|
end
|
|
|
|
def column_names_for(klass)
|
|
klass.column_names
|
|
rescue ::ActiveRecord::StatementInvalid
|
|
[]
|
|
end
|
|
|
|
def belongs_to_required_by_default?
|
|
::ActiveRecord::Base.belongs_to_required_by_default
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|