Should I test that?

Should I test model validations?

Verdict

Yes

Test each validation your model declares with one valid and one invalid record; do not test that the framework's own validators, such as Rails presence or Django MaxLengthValidator, work.

Why

Test the validations your model declares, and skip tests of the validator code itself. The typical case is a Rails or Django model with presence, format and length rules plus one custom rule, such as an end date after the start date. Blast radius is users and Change frequency is regularly, because invalid records show customers wrong data and models gain or relax rules with most features. Detectability is eventually, because a deleted or narrowed validation lets invalid records save without an error. Reversibility is with-effort, because bad records stay until a repair script finds them, and Test cost is moderate, because each test needs a factory that changes with the model, so rule R11 gives Test.

When the decision changes
WhenDecisionWhy
A NOT NULL column or a unique index in the database also enforces the validationTest minimally: one test that the database rejects the invalid recordDetectability moves to same-day and Reversibility to trivial, because the database refuses the bad row
The validation caps a refund at the order totalTest mandatory: the valid amount, the invalid amount and the exact boundaryBlast radius rises to money and Reversibility to costly, because a paid-out refund needs manual recovery
The validation requires parental consent for users under 16Test mandatory, including the age boundary and every path that creates an accountBlast radius rises to safety-or-legal, Detectability to never and Reversibility to costly, because an account without consent looks like any other
The model belongs to an internal admin tool that only staff useTest minimally: one test per model that an invalid record is rejectedBlast radius falls to internal, because no customer sees a bad record
The model lives in a prototype that only you use and delete after the demoDo not test; submit the form once with bad input after each changeBlast radius falls to none
The validation test calls valid? on an unsaved object and needs no databaseTest, with the same valid and invalid pair per ruleTest cost falls to trivial, but Detectability eventually and Reversibility with-effort still set the decision

What breaks if you don't test

The Rails suite tests validates :email, presence: true, but it does not know your User model has that line. A validation disappears in a merge, gains on: :create and stops checking updates, or gets allow_blank: true so an empty email passes the format check. The record still saves. The invalid rows surface weeks later, when a mailer or an export fails on them, and every record saved since the change needs a repair.

What you lose if you over-test

The common over-test checks the framework: that length: { maximum: 100 } rejects 101 characters, or that the error message reads exactly "can't be blank". A test of framework behaviour repeats the declaration or pins a translation string, so a copy change breaks the suite while the rule still holds. A green validation test also says nothing about paths that skip validations: update_column and insert_all in Rails, or save() in Django, which runs no validators unless something calls full_clean().

How to test

Write model tests that build one valid record, change one attribute per test, and assert an error on that attribute rather than its message text. Cover each custom rule at its boundary, such as an end date equal to the start date. For uniqueness and required fields, add a database constraint and test once that the database rejects the duplicate or the null, because the Rails validations guide warns that two connections can both pass a uniqueness check. For Django, the validators reference lists which calls run validators.

When the answer changes

  • The validation guards money, age, consent or another rule that a law or a contract sets.
  • The database already enforces the rule with a constraint.
  • Imports or background jobs write records through calls that skip validations.

Real incident + Code example

The duplicate accounts behind a green uniqueness test

On a Rails shop I worked on, User declared validates :email, uniqueness: { case_sensitive: false }, and a shoulda-matchers spec confirmed the declaration. The users table had no unique index. On slow mobile connections customers tapped the signup button twice, both requests passed the uniqueness check before either inserted, and over two months 214 customers got two accounts each. Sign-in found the first account while orders went to the second, so those customers saw an empty order history until support tickets exposed the problem. We added a unique index on lower(email), merged the accounts with a script, and added a spec that the database refuses a duplicate even when validations are skipped:

RSpec.describe User do
  it "requires an email" do
    user = build(:user, email: " ")
    expect(user).not_to be_valid
    expect(user.errors).to include(:email)
  end

  it "rejects a duplicate email in any case" do
    create(:user, email: "ana@example.com")
    expect(build(:user, email: "ANA@example.com")).not_to be_valid
  end

  it "has a unique index that holds when validations are skipped" do
    create(:user, email: "ana@example.com")
    duplicate = build(:user, email: "Ana@Example.com")
    expect { duplicate.save(validate: false) }
      .to raise_error(ActiveRecord::RecordNotUnique)
  end
end

FAQ

Should native validations be tested in Rails?

Yes, test that your model declares each native validation, with one valid and one invalid record per rule. Do not test that the Rails presence or length validators work, because the Rails suite already covers them.

Is it necessary to unit test ActiveRecord validations?

Yes, ActiveRecord validations need tests, because a removed validation saves invalid records without an error. Most of these tests call valid? on an unsaved record and need no database; uniqueness needs a test database with a unique index.

Should I use shoulda-matchers for model validations?

Yes, a shoulda-matchers one-liner such as validate_presence_of(:email) catches a deleted Rails validation at almost no cost. The matcher checks the declaration only, so a custom rule still needs a valid and an invalid record.