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:
- Make a model instance (named
record
below) - Run validations by saying
record.validate
- Check if
record.errors[:attribute_being_tested]
contains the expected validation error - Put the attribute into a valid state
- Run validations again by saying
record.validate
- 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)