In my Rails6 app I've got two model validations which I want to test by Minitest:
class Portfolio < ApplicationRecord
validates :name, :status, presence: true
validates :initial_return do |record, attr, value|
record.errors.add(attr, 'Add value between -100 and 100') unless value >= -100 && value <= 100
end
end
Minitest:
class PortfolioTest < ActiveSupport::TestCase
setup do
@portfolio = Portfolio.create(name: Faker::Bank.name)
end
test 'invalid PortfolioSpotlightFigure, does not fit the range (-100, 100)' do
@portfolio.initial_return = -101
assert_not @portfolio.valid?
@portfolio.initial_return = 101
assert_not @portfolio.valid?
@portfolio.initial_return = 50
assert @portfolio.valid?
end
context 'validations' do
should validate_presence_of(:name)
end
end
Minitest gives the same error for both cases:
ArgumentError: You need to supply at least one validation
But when I remove validation for :initial_return
field from Portfolio
model:
validates :initial_return do |record, attr, value|
record.errors.add(attr, 'Add value between -100 and 100') unless value >= -100 && value <= 100
the test will pass for the validate_presence_of(:name)
which means that I incorrectly defined that validation. What did I missed?
You don't need to reinvent the wheel
class Portfolio < ApplicationRecord
validates :name, :status, presence: true
validates :initial_return,
numericality: {
greater_than_or_equal_to: -100,
less_than_or_equal_to: 100
}
end
And stop carpet bombing your validations in your tests. Test the actual validation and not if the entire object is valid/invalid which leads to false positives and negatives. For example:
test 'invalid PortfolioSpotlightFigure, does not fit the range (-100, 100)' do
@portfolio.initial_return = -101
# these will pass even if you comment out the validation on initial_return as
# status is nil
assert_not @portfolio.valid?
@portfolio.initial_return = 101
assert_not @portfolio.valid?
# Will fail because status is nil
@portfolio.initial_return = 50
assert @portfolio.valid?
end
As you can see the test failures will tell you nothing about why the model is valid/invalid.
Instead use one assertion per test and test the actual validation:
class PortfolioTest < ActiveSupport::TestCase
setup do
# you dont need to insert records into the db to test associations
@portfolio = Portfolio.new
end
test 'initial return over 100 is invalid' do
# arrange
@portfolio.initial_return = 200
# act
@portfolio.valid?
# assert
assert_includes(@portfolio.errors.full_messages, "Initial return must be less than or equal to 100")
end
test 'initial return below -100 is invalid' do
# arrange
@portfolio.initial_return = -200
# act
@portfolio.valid?
# assert
assert_includes(@portfolio.errors.full_messages, "Initial return must be greater than or equal to -100")
end
test 'an initial return between -100 and 100 is valid' do
# arrange
@portfolio.initial_return = 50
# act
@portfolio.valid?
# assert
refute(@portfolio.errors.has_key?(:intial_return))
end
# ...
end
With shoulda you should be able to use the validates_numericality_of
matcher:
should validate_numericality_of(:initial_return).
is_greater_than_or_equal_to(-100).
is_less_than_or_equal_to(100)