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_to_not 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.

Note that in RSpec 3's expect syntax, you use is_expected.to for the shorthand it { should ... } syntax:

describe User do

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

end

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_to_not 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
Jakob Scholz
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2016-02-22 12:41)