Skip to content

Commit

Permalink
uniqueness: Guide users to set non-nullable attrs
Browse files Browse the repository at this point in the history
When attempting to create an existing record, if the database raises an
error because a non-nullable column is not set, then capture this error
and raise a more useful one instead that guides users to provide a
record where the column is already set.
  • Loading branch information
mcmire committed Jan 18, 2016
1 parent 45de869 commit 78ccfc5
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 78ccfc5

Please sign in to comment.