Add without_presence_validation q to belong_to

With the new Rails 5 behavior, `belong_to` will check to ensure that the
association has a presence validation on it. In some cases, however,
this is not desirable. For instance, say we have this setup:

    class Employee < ApplicationRecord
      # Assume belongs_to_required_by_default is true
      belongs_to :manager

      before_validation :add_manager

      private

      def add_manager
        self.manager = Manager.create
      end
    end

In this case, even though the association is effectively defined with
`required: true`, the ensuing presence validation never fails, because
`manager` is always set to something before validations kick off. So
this test won't work:

    it { should belong_to(:manager) }

To get around this, this commit allows us to say:

    it { should belong_to(:manager).without_presence_validation }

which instructs the matcher not to test for any presence (or absence,
for that matter) of a presence validation, mimicking the pre-Rails 5
behavior.
This commit is contained in:
Elliot Winkler 2019-01-29 01:19:12 -07:00
parent ca6a1ff9ee
commit 707d87dbd6
3 changed files with 140 additions and 0 deletions

View File

@ -91,6 +91,11 @@ is now:
* *Original PR: [#956]*
* *Original issues: [#870], [#861]*
* Add `without_presence_validation` qualifier to `belong_to` to get around the
fact that `required` is assumed, above.
* *Original issues: [#1153], [#1154]*
* Add `allow_nil` qualifier to `delegate_method`.
* *Commit: [d49cfca]*
@ -147,6 +152,8 @@ is now:
[#956]: https://github.com/thoughtbot/shoulda-matchers/pulls/956
[#870]: https://github.com/thoughtbot/shoulda-matchers/issues/870
[#861]: https://github.com/thoughtbot/shoulda-matchers/issues/861
[#1153]: https://github.com/thoughtbot/shoulda-matchers/issues/1153
[#1154]: https://github.com/thoughtbot/shoulda-matchers/issues/1154
[#954]: https://github.com/thoughtbot/shoulda-matchers/issues/954
[#1074]: https://github.com/thoughtbot/shoulda-matchers/pulls/1074
[#1075]: https://github.com/thoughtbot/shoulda-matchers/pulls/1075

View File

@ -272,6 +272,35 @@ module Shoulda
# should belong_to(:organization).required
# end
#
# #### without_presence_validation
#
# Use `without_presence_validation` 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_presence_validation }
# end
#
# # Minitest (Shoulda)
# class PersonTest < ActiveSupport::TestCase
# should belong_to(:organization).without_presence_validation
# end
#
# ##### optional
#
# Use `optional` to assert that the association is allowed to be nil.
@ -1092,6 +1121,11 @@ module Shoulda
self
end
def without_validating_presence
remove_submatcher(AssociationMatchers::RequiredMatcher)
self
end
def description
description = "#{macro_description} #{name}"
description += " class_name => #{options[:class_name]}" if options.key?(:class_name)

View File

@ -616,6 +616,105 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
end
end
if active_record_supports_optional_for_associations?
context 'when the model ensures the association is set' do
context 'and the matcher is not qualified with anything' do
context 'and the matcher is not qualified with without_validating_presence' do
it 'fails with an appropriate message' do
model = create_child_model_belonging_to_parent do
before_validation :ensure_parent_is_set
def ensure_parent_is_set
self.parent = Parent.create
end
end
assertion = lambda do
configuring_default_belongs_to_requiredness(true) do
expect(model.new).to belong_to(:parent)
end
end
message = format_message(<<-MESSAGE, one_line: true)
Expected Child to have a belongs_to association called parent (and
for the record to fail validation if :parent is unset; i.e.,
either the association should have been defined with `required:
true`, or there should be a presence validation on :parent)
MESSAGE
expect(&assertion).to fail_with_message(message)
end
end
context 'and the matcher is qualified with without_validating_presence' do
it 'passes' do
model = create_child_model_belonging_to_parent do
before_validation :ensure_parent_is_set
def ensure_parent_is_set
self.parent = Parent.create
end
end
configuring_default_belongs_to_requiredness(true) do
expect(model.new).
to belong_to(:parent).
without_validating_presence
end
end
end
end
context 'and the matcher is qualified with required' do
context 'and the matcher is not qualified with without_validating_presence' do
it 'fails with an appropriate message' do
model = create_child_model_belonging_to_parent do
before_validation :ensure_parent_is_set
def ensure_parent_is_set
self.parent = Parent.create
end
end
assertion = lambda do
configuring_default_belongs_to_requiredness(true) do
expect(model.new).to belong_to(:parent).required
end
end
message = format_message(<<-MESSAGE, one_line: true)
Expected Child to have a belongs_to association called parent
(and for the record to fail validation if :parent is unset; i.e.,
either the association should have been defined with `required:
true`, or there should be a presence validation on :parent)
MESSAGE
expect(&assertion).to fail_with_message(message)
end
end
context 'and the matcher is also qualified with without_validating_presence' do
it 'passes' do
model = create_child_model_belonging_to_parent do
before_validation :ensure_parent_is_set
def ensure_parent_is_set
self.parent = Parent.create
end
end
configuring_default_belongs_to_requiredness(true) do
expect(model.new).
to belong_to(:parent).
required.
without_validating_presence
end
end
end
end
end
end
def belonging_to_parent(options = {}, parent_options = {}, &block)
child_model = create_child_model_belonging_to_parent(
options,