From 707d87dbd60d2b1781f4ea2e9dab73e207d65173 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Tue, 29 Jan 2019 01:19:12 -0700 Subject: [PATCH] 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. --- NEWS.md | 7 ++ .../active_record/association_matcher.rb | 34 +++++++ .../active_record/association_matcher_spec.rb | 99 +++++++++++++++++++ 3 files changed, 140 insertions(+) diff --git a/NEWS.md b/NEWS.md index 31914aa4..68bdd3f3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/lib/shoulda/matchers/active_record/association_matcher.rb b/lib/shoulda/matchers/active_record/association_matcher.rb index f046a97b..df795831 100644 --- a/lib/shoulda/matchers/active_record/association_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matcher.rb @@ -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) diff --git a/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb index dcb3d14f..f13368c8 100644 --- a/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb @@ -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,