I have a Ride model with price float field and validation of the precision. I want to display my own custom error message when the validation fails but it doesn't work.
According to Rails Gudes "the :message option lets you specify the message that will be added to the errors collection when validation fails. When this option is not used, Active Record will use the respective default error message for each validation helper. The :message option accepts a String or Proc."
I do it exactly as in the example there and it does not work.
Rails guides
validates :age, numericality: { message: "%{value} seems wrong" }
My example
validates :price, numericality: { message: "Invalid price. Max 2 digits after period"}, format: { with: /\A\d{1,4}(.\d{0,2})?\z/ }
spec/models/ride_spec.rb
context 'with more than 2 digits after period' do
let(:price) { 29.6786745 }
it 'the price is invalid' do
expect(subject.save).to be_falsy
expect(subject).not_to be_persisted
puts subject.errors.full_messages.last # "Price is invalid"
end
end
What am I doing wrong?
Update
This is what I've learned so far.
I have set the price to be empty in the test and it now shows the error message that I want.
context 'with more than 2 digits after period' do
let(:price) { '' }
it 'the price is invalid' do
expect(subject.save).to be_falsy
expect(subject).not_to be_persisted
puts subject.errors.full_messages.last # "Price Invalid price. Max 2 digits after period"
end
end
Conslusion: it works for 'presence' validation, not for numericality validation, which is very confusing as the docs say clearly that you validate numericality, not presence. Am I right? Is this an error or deliberate?
I think where you are going wrong is expecting that numericality accepts a validation option format. Referring to the active record guides there is no option for format.
Seeing that you have called this price it seems you want to keep the precision to 2 decimal places so you can store the dollar value of something. The proper type for this is a decimal with scale: 2, or something I've had success with in the past is storing the price as an integer price_in_cents.
context 'with more than 2 digits after period' do
let(:price) { 123.333 }
it 'rounds to 2 decimal places' do
expect(subject.save).to eq true
expect(subject.reload.price).to eq 123.34
end
end
I figured this out, there are two validations: format validation and numericality validation. I did not add message to format validation, so when it fails I get the standard message
validates :price, format: { with: /\A\d{1,4}(.\d{0,2})?\z/, message: 'Invalid price. Max 2 digits after period'}, numericality: { message: 'is not a number' }
Related
Model:
validates :email,
uniqueness: {
message: "has been taken."
},
presence: {
message: "cannot be blank."
},
length: {
minimum: 3,
message: "is too short, must be a minimum of 3 characters.",
allow_blank: true
},
format: {
with: /\A[A-Z0-9_\.&%\+\-']+#(?:[A-Z0-9\-]+\.)+(?:[A-Z]{2,13})\z/i,
message: "is invalid.",
if: Proc.new { |u| u.email && u.email.length >= 3 }
}
RSpec:
before(:each) do
#user = FactoryGirl.build(:user)
end
it { should validate_length_of(:email).is_at_least(3) }
Error:
Failure/Error: should validate_length_of(:email).is_at_least(3)
Expected errors to include "is too short (minimum is 3 characters)" when email is set to "xx",
got errors:
* "is too short (minimum is 4 characters)" (attribute: password, value: nil)
* "is too short, must be a minimum of 3 characters." (attribute: email, value: "xx")
* "is not included in the list" (attribute: state, value: "passive")
Factory:
factory :user, class: User do
email FFaker::Internet.email
password FFaker::Internet.password
username FFaker::Internet.user_name
end
I am using factory_girl_rails with shoulda_matchers. Every time I try to validate my email, I keep getting an error like above. It says the email value is "xx", but the length of the factory email is greater than that. How can I write an rspec that will pass?
Shoulda-matchers tests validations by testing the messages in the errors object.
The valdation matchers only work with the rails default error messages unless you specify otherwise:
it { should validate_length_of(:email).with_message("is too short, must be a minimum of 3 characters.") }
The with_message method is detailed in this comment.
You are understanding the error (and it's cause) wrong. The error says that it expects "is too short (minimum is 3 characters)" but in the errors it can't find that string (rspec finds "is too short, must be a minimum of 3 characters." which is what you defined on your validation).
When you use shoulda matchers and say
it { should validate_length_of(:email).is_at_least(3) }
I'm guessing it creates a test with an email shorter than 3 and checks if it fails, that's why it's ignoring your factory, internally should matcher is setting a fixed length string just to make that test pass.
When you see the errors in user, that test should actually work since the error is actually there, only the string is different. So, you have two options: remove the custom message when length is minimum: 3; or tell the matcher what message you are expecting:
it { should validate_length_of(:email).is_at_least(3).with_message("is too short, must be a minimum of 3 characters.") }
Given validation on a field such as the following:validates :entity_type, inclusion: { in: %w(1 2), message: "is invalid" }
The error messages that will be returned if the user enters nil for the field are
“can’t be blank”, ” is invalid”
How can this validation be changed so that only 'can't be blank' is returned if nil is the entered value for the field?
This is a case where we don't want nil to be a valid value, just to clean up the validation messaging.
I haven't seen any documentation on this anywhere so wanted to give an answer.
The answer is to use allow_nil: true with the inclusion validator.
validates :entity_type, inclusion: { in: %w(1 2), message: "is invalid" }, allow_nil: true
Using this means if a nil value is given for the field, only the 'can't be blank' message will be returned from the validator, while non-nil invalid values will continue to work as normal.
I have the Order model with the line for calculating total total price of products the user is ordering
before_validation :set_total!
validates :total, presence: true, numericality: { greater_than_or_equal_to: 0 }
set_total looks like this
def set_total!
self.total = products.map(&price).sum
end
On my specs I am trying to check if the total validations are implemented TDD
it { should validate_presence_of(:total) }
it { should validate_numericality_of(:total).is_greater_than_or_equal_to(0) }
Unfortunately I am receiving the following error
Failure/Error: it { should validate_presence_of(:total) }
Order did not properly validate that :total cannot be empty/falsy.
After setting :total to ‹nil› -- which was read back as
‹#<BigDecimal:5634b8b81008,'0.0',9(27)>› -- the matcher expected the
Order to be invalid, but it was valid instead.
As indicated in the message above, :total seems to be changing certain
values as they are set, and this could have something to do with why
this test is failing. If you've overridden the writer method for this
attribute, then you may need to change it to make this test pass, or
do something else entirely.
How can I fix this?
Using the validate_presence_of matcher is roughly equivalent to writing this test by hand:
describe Order do
it "fails validation when total is nil" do
order = Order.new
order.total = nil
order.validate
expect(order.errors[:total]).to include("can't be blank")
order.total = 42
expect(order.errors[:total]).not_to include("can't be blank")
end
end
If you were to run this test, you would find that this would fail. Why? Because in your model, you set total to a non-nil value when validations are performed. That's why you're getting this error.
So you don't really need the validation or the matcher, since neither one would fail.
I want a (numerical) model attribute foo to be validated as
(1) present, and
(2) be greater than or equal to 0.
Since (1) is a prerequisite to (2), in case no value is given for parameter foo, I want the validation to report only the error related to (1), and not (2).
I tried to do it like this:
validates :foo_attribute,
numericality: {greater_than_or_equal_to: 0},
presence: true
But when the given value of parameter :foo is absent, I get messages in errors that originate from both validations (1) and (2).
In such case, how can I get only the validation error related to (1) and not (2)?
Looks like internally validates() just splits it into multiple validations:
def validates(*attributes)
defaults = attributes.extract_options!.dup
validations = defaults.slice!(*_validates_default_keys)
raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
raise ArgumentError, "You need to supply at least one validation" if validations.empty?
defaults[:attributes] = attributes
validations.each do |key, options|
next unless options
key = "#{key.to_s.camelize}Validator"
begin
validator = key.include?('::') ? key.constantize : const_get(key)
rescue NameError
raise ArgumentError, "Unknown validator: '#{key}'"
end
validates_with(validator, defaults.merge(_parse_validates_options(options)))
end
end
That being the case, you would have to either write your own method or do something like:
validates_presence_of :foo_attribute
validates_numericality_of :foo_attribute, greater_than: 0, unless: Proc.new { |foo_instance| foo_instance.foo_attribute.nil? }
I'm having an issue with Validations, if I use the following syntax all is well (no failures).
validates :title, uniqueness: true
However if I change it to this, I get a failure.
validates :title, uniqueness: {message: 'Title must be unique'}
Here is the test for completeness:
test "product is not valid without a unique title " do
product = Product.new( title: products(:ruby).title,
description: "yyy",
price: 1,
image_url: "fred.gif" )
assert !product.save
assert_equal "has already been taken", product.errors['title'].join('; ')
end
I have a fixture that adds the book title for the Ruby product etc.
As I understand it the two validations should be the same, just that one gives a custom error message. This is the error I get when using the custom message.
1) Failure:
test_product_is_not_valid_without_a_unique_title_(ProductTest)
<"has already been taken"> expected but was
<"Title must be unique">.
Thanks in advance for your help.
Here:
assert_equal "has already been taken", product.errors['title'].join('; ')
you expect to see "has already been taken" in the errors hash, but you've overridden this message with your custom message. Test shows that your custom message appears as it should. Why do you still expect the default message? You have to expect Title must be unique.
TIP Don't supply the name of your field in your custom message. Rails will handle it automatically when generating errors by, say, product.errors.full_messages ("must be unique" would be enough).