I have created a custom validator which has it's own specific unit tests to check that it works.
Using should-matchers there was a suggestion to add a validates_with
matcher, so you could write:
subject.validates_with(:custom_validator)
Quite rightly the suggestion was declined, since it does not really test the behaviour of the model.
But my model has 4 fields that use the custom validator, and I want that behaviour to be tested - ie that those 4 fields are being validated, just as I am testing that they are being validated for presence:
describe '#attribute_name' do
it { is_expected.to validate_presence_of(:attribute_name) }
end
So how can I write a test that basically does the same thing, something sort of like this:
describe '#attribute_name' do
it { is_expected.to use_custom_validator_on(:attribute_name) }
end
This question asks the same thing and the answer suggests building a test model. However, my validator requires an option, it is used like this:
\app\models\fund.rb
class Fund < ActiveRecord
validates :ein, digits: { exactly: 9 }
end
So if I build a test model, and test it as suggested:
it 'is has correct number of digits' do
expect(build(:fund, ein: '123456789')).to be_valid
end
it 'is has incorrect number of digits' do
expect(build(:fund, ein: '123').to be_invalid
end
I receive RecordInvalid
error (from my own validator! lol) saying I did not supply the required option for the validator. That option is called 'exactly'.
1) Fund#ein validates digits
Failure/Error: raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
ActiveRecord::RecordInvalid:
Record invalid
So is Rspec not 'seeing' the value '9' defined in the model file?
Obviously it makes no sense to define that in the test as that is the defined behaviour I am trying to test for. Think of it like the validates_length_of
testing for the { length: x }
option.
Surely there must be a way to test that this custom validator option is set on the model?
The validator code
class DigitsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
length = options[:exactly]
regex = /\A(?!0{#{length}})\d{#{length}}\z/
return unless value.scan(regex).empty?
record.errors[attribute] << (options[:message] || error_msg(length))
end
private
def error_msg(length)
I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
end
end
Interesting side note
Obviously if I remove the 'raise' line from the DigitsValidator
then both the tests succeed. Is there something wrong with my code that I cannot see?
I think you would have to add a return statement, no? :-)
def error_msg(length)
return I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
end
Alternatively, remove that method and use a guard after setting length
:
class DigitsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
length = options[:exactly]
raise ActiveRecord::RecordInvalid if length.nil?
regex = /\A(?!0{#{length}})\d{#{length}}\z/
return unless value.scan(regex).empty?
record.errors[attribute] <<
(options[:message] ||
I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length))
end
end