ruby-on-railsrubyminitestshoulda

Rails Minitest one model validation causes ArgumentError: You need to supply at least one validation


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?


Solution

  • 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)