diff --git a/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb b/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb index 26adfb528..7f7a58517 100644 --- a/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +++ b/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb @@ -53,23 +53,34 @@ module ActiveRecord # it { should validate_uniqueness_of(:title) } # end # - # However, running this test will fail with something like: + # However, running this test will fail with an exception such as: # - # Failures: + # Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher::ExistingRecordInvalid: + # validate_uniqueness_of works by matching a new record against an + # existing record. If there is no existing record, it will create one + # using the record you provide. # - # 1) Post should validate :title to be case-sensitively unique - # Failure/Error: it { should validate_uniqueness_of(:title) } - # ActiveRecord::StatementInvalid: - # SQLite3::ConstraintException: posts.content may not be NULL: INSERT INTO "posts" ("title") VALUES (?) + # While doing this, the following error was raised: + # + # PG::NotNullViolation: ERROR: null value in column "content" violates not-null constraint + # DETAIL: Failing row contains (1, null, null). + # : INSERT INTO "posts" DEFAULT VALUES RETURNING "id" + # + # The best way to fix this is to provide the matcher with a record where + # any required attributes are filled in with valid values beforehand. + # + # (The exact error message will differ depending on which database you're + # using, but you get the idea.) # # This happens because `validate_uniqueness_of` tries to create a new post # but cannot do so because of the `content` attribute: though unrelated to - # this test, it nevertheless needs to be filled in. The solution is to - # build a custom Post object ahead of time with `content` filled in: + # this test, it nevertheless needs to be filled in. As indicated at the + # end of the error message, the solution is to build a custom Post object + # ahead of time with `content` filled in: # # describe Post do # describe "validations" do - # subject { Post.new(content: 'Here is the content') } + # subject { Post.new(content: "Here is the content") } # it { should validate_uniqueness_of(:title) } # end # end @@ -468,6 +479,8 @@ def create_existing_record ensure_secure_password_set(existing_record) existing_record.save(validate: false) end + rescue ::ActiveRecord::StatementInvalid => error + raise ExistingRecordInvalid.create(underlying_exception: error) end def ensure_secure_password_set(instance) @@ -896,6 +909,27 @@ def message MESSAGE end end + + class ExistingRecordInvalid < Shoulda::Matchers::Error + include Shoulda::Matchers::ActiveModel::Helpers + + attr_accessor :underlying_exception + + def message + <<-MESSAGE.strip +validate_uniqueness_of works by matching a new record against an +existing record. If there is no existing record, it will create one +using the record you provide. + +While doing this, the following error was raised: + +#{Shoulda::Matchers::Util.indent(underlying_exception.message, 2)} + +The best way to fix this is to provide the matcher with a record where +any required attributes are filled in with valid values beforehand. + MESSAGE + end + end end end end diff --git a/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb index f75817af6..191b44afa 100644 --- a/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb @@ -327,18 +327,39 @@ end end - context 'and the table has non-nullable columns other than the attribute being validated, set beforehand' do - it 'can save the subject without the attributes being set' do - options = { - additional_attributes: [ - { name: :required_attribute, options: { null: false } } - ] - } - model = define_model_validating_uniqueness(options) - record = model.new - record.required_attribute = 'something' + context 'and the table has non-nullable columns other than the attribute being validated' do + context 'which are set beforehand' do + it 'can save the subject' do + options = { + additional_attributes: [ + { name: :required_attribute, options: { null: false } } + ] + } + model = define_model_validating_uniqueness(options) + record = model.new + record.required_attribute = 'something' + + expect(record).to validate_uniqueness + end + end - expect(record).to validate_uniqueness + context 'which are not set beforehand' do + it 'raises a useful exception' do + options = { + additional_attributes: [ + { name: :required_attribute, options: { null: false } } + ] + } + model = define_model_validating_uniqueness(options) + + assertion = lambda do + expect(model.new).to validate_uniqueness + end + + expect(&assertion).to raise_error( + described_class::ExistingRecordInvalid + ) + end end end