Testing ActiveRecord validations with RSpec

Updated . Posted . Visible to the public. Repeats.

Validations should be covered by a model's spec.

This card shows how to test an individual validation. This is preferrable to save an entire record and see whether it is invalid.

Recipe for testing any validation

In general any validation test for an attribute :attribute_being_tested looks like this:

  1. Make a model instance (named record below)
  2. Run validations by saying record.validate
  3. Check if record.errors[:attribute_being_tested] contains the expected validation error
  4. Put the attribute into a valid state
  5. Run validations again by saying record.validate
  6. Check if record.errors[:attribute_being_tested] does not contain the expected validation error

For instance, we want to test that User#email is present:

describe User do

  describe '#email' do
    it 'validates presence' do
      record = User.new
      record.email = '' # invalid state
      record.validate 
      expect(record.errors[:email]).to include("can't be blank") # check for presence of error

      record.email = 'foo@bar.com' # valid state
      record.validate 
      expect(record.errors[:email]).to_not include("can't be blank") # check for absence of error
    end
  end
  
end

Standard validations can be tested with less code

The shoulda-matchers Show archive.org snapshot gem gives you some RSpec matchers to test the application of standard Rails validations. Under the hood should-matchers uses the same recipe as outlined above (set invalid state, run validations, check for message, etc.), but your tests will have a lot less code:

    describe User do

      describe '#screen_name' do
        it { is_expected.to validate_presence_of(:screen_name) }
      end

      describe '#email' do
        it { is_expected.not_to allow_value("blah").for(:email) }
        it { is_expected.to allow_value("a@b.com").for(:email) }
      end

      describe '#age' do
        it { is_expected.to validate_inclusion_of(:age).in_range(1..100) }
      end

    end

See the shoulda-matchers README Show archive.org snapshot for a full list of matchers provided. See our dedicated card for should validate_uniqueness_of, it works differently than you think.

Where to put validation examples

We usually sort our specs by method name.

While a case can be made by putting validation tests under describe '#validate', I prefer to sort validation tests under the example group for the method that is being validated. This way they will be right next to defaults and other behavior for that method:

describe Report do

  describe '#currency' do
    it { is_expected.to allow_values('EUR', 'USD', 'GBP', 'CHF').for(:currency) }

    it { is_expected.not_to allow_values('foo', '€', '', nil).for(:currency) }

    it 'defaults to EUR' do
      expect(subject.currency).to eq('EUR')
    end
  end

  describe '#year' do
    it { is_expected.to validate_presence_of(:year) }

    it 'defaults to the current year' do
      expect(subject.year).to eq(Date.today.year)
    end
  end

end

Testing custom validation methods

The recipe above applies to custom validation methods as well.

Let's say you want to test that User#screen_name is not a palindrome Show archive.org snapshot . Since that check is not possible with standard Rails validations, we write a custom validation method like this:

class User < ActiveRecord::Base

  validate :validate_screen_name_is_no_palindrome

  private

  def validate_screen_name_is_no_palindrome
    if screen_name.downcase == screen_name.downcase.reverse
      errors.add(:screen_name, 'must not be a palindrome')
    end
  end

end

We can now reply the test recipe from above:

describe User do

  describe '#screen_name' do

    it "validates that it's not a palindrome" do
      subject.screen_name = 'Hannah'
      subject.validate
      expect(subject.errors[:screen_name]).to include('must not be a palindrome')
      subject.screen_name = 'Johanna'
      subject.validate
      expect(subject.errors[:screen_name]).to_not include('must not be a palindrome')
    end

  end

end

Different ways of testing errors

Checking the human errors messages

subject.validate
expect(subject.errors(:bar)).to contain_exactly('some error')
subject.validate
expect(subject.errors(:bar)).to eq(['some error'])

Checking the error message key

subject.validate
expect(subject.errors).to be_of_kind(:bar, :invalid)
Henning Koch
Last edit
Niklas Hasselmeyer
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2016-02-22 12:41)