RoR Model Tests

Model tests verify that your data layer works correctly. They test validations, associations, scopes, and custom methods. Model tests run fast because they talk directly to the database without going through routes, controllers, or views. A thorough model test suite catches data bugs before they ever reach users.

What to Test in a Model

Model areas to test:
  1. Validations        ← does invalid data get rejected?
  2. Associations       ← do relationships behave correctly?
  3. Scopes             ← do named queries return the right records?
  4. Custom methods     ← do business logic methods return correct values?
  5. Callbacks          ← does before_save / after_create run correctly?

Testing Validations

spec/models/product_spec.rb

require "rails_helper"

RSpec.describe Product, type: :model do
  describe "validations" do
    let(:product) { build(:product) }

    it "is valid with valid attributes" do
      expect(product).to be_valid
    end

    it "requires a name" do
      product.name = nil
      expect(product).not_to be_valid
      expect(product.errors[:name]).to include("can't be blank")
    end

    it "requires a positive price" do
      product.price = -5
      expect(product).not_to be_valid
      expect(product.errors[:price]).to include("must be greater than 0")
    end

    it "requires price to be a number" do
      product.price = "free"
      expect(product).not_to be_valid
    end

    it "requires stock to be zero or more" do
      product.stock = -1
      expect(product).not_to be_valid
    end

    it "requires a unique SKU" do
      create(:product, sku: "ABC123")
      duplicate = build(:product, sku: "ABC123")
      expect(duplicate).not_to be_valid
    end
  end
end

Testing Associations

RSpec.describe User, type: :model do
  describe "associations" do
    let(:user) { create(:user) }

    it "has many articles" do
      article1 = create(:article, user: user)
      article2 = create(:article, user: user)
      expect(user.articles).to include(article1, article2)
    end

    it "destroys associated articles when deleted" do
      create(:article, user: user)
      expect { user.destroy }.to change(Article, :count).by(-1)
    end

    it "has one profile" do
      profile = create(:profile, user: user)
      expect(user.profile).to eq(profile)
    end
  end
end

Testing Scopes

RSpec.describe Article, type: :model do
  describe "scopes" do
    before do
      create(:article, published: true,  created_at: 3.days.ago)
      create(:article, published: true,  created_at: 1.day.ago)
      create(:article, published: false, created_at: 2.days.ago)
    end

    describe ".published" do
      it "returns only published articles" do
        expect(Article.published.count).to eq(2)
      end

      it "excludes unpublished articles" do
        unpublished = Article.where(published: false).first
        expect(Article.published).not_to include(unpublished)
      end
    end

    describe ".recent" do
      it "returns articles ordered newest first" do
        articles = Article.published.recent
        expect(articles.first.created_at).to be > articles.last.created_at
      end
    end
  end
end

Testing Custom Methods

RSpec.describe User, type: :model do
  describe "#full_name" do
    it "returns first and last name joined" do
      user = build(:user, first_name: "Alice", last_name: "Smith")
      expect(user.full_name).to eq("Alice Smith")
    end
  end

  describe "#admin?" do
    it "returns true for admin role" do
      user = build(:user, role: :admin)
      expect(user.admin?).to be true
    end

    it "returns false for viewer role" do
      user = build(:user, role: :viewer)
      expect(user.admin?).to be false
    end
  end

  describe "#initials" do
    it "returns uppercase initials from full name" do
      user = build(:user, first_name: "Alice", last_name: "Smith")
      expect(user.initials).to eq("AS")
    end
  end
end

Testing Callbacks

RSpec.describe Article, type: :model do
  describe "callbacks" do
    describe "before_save" do
      it "generates a slug from the title before saving" do
        article = create(:article, title: "My First Post")
        expect(article.slug).to eq("my-first-post")
      end

      it "updates the slug when the title changes" do
        article = create(:article, title: "Old Title")
        article.update(title: "New Title")
        expect(article.slug).to eq("new-title")
      end
    end

    describe "after_create" do
      it "sends a welcome email after user creation" do
        expect {
          create(:user, email: "new@example.com")
        }.to have_enqueued_mail(UserMailer, :welcome_email)
      end
    end
  end
end

Testing Database Interactions

RSpec.describe Order, type: :model do
  describe ".total_revenue" do
    it "sums the amount of all completed orders" do
      create(:order, amount: 100, status: "completed")
      create(:order, amount: 250, status: "completed")
      create(:order, amount: 50,  status: "pending")   ← excluded

      expect(Order.total_revenue).to eq(350)
    end
  end

  describe "#apply_discount" do
    it "reduces the order amount by the given percentage" do
      order = create(:order, amount: 200)
      order.apply_discount(10)   ← 10% off
      expect(order.amount).to eq(180)
    end
  end
end

Database Cleaner — Fresh Database Per Test

Each test should start with a clean database. Add DatabaseCleaner to prevent test data from leaking between tests:

gem "database_cleaner-active_record"
bundle install
spec/rails_helper.rb

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end

  config.around(:each) do |example|
    DatabaseCleaner.cleaning { example.run }
  end
end

Shoulda Matchers — Concise Validation Tests

Shoulda Matchers provide one-line validation and association tests:

gem "shoulda-matchers"

RSpec.describe User, type: :model do
  it { should validate_presence_of(:name) }
  it { should validate_presence_of(:email) }
  it { should validate_uniqueness_of(:email) }
  it { should validate_length_of(:password).is_at_least(8) }
  it { should have_many(:articles).dependent(:destroy) }
  it { should have_one(:profile) }
  it { should belong_to(:organization) }
end

Each line replaces four to six lines of standard RSpec. Use them for simple validation checks and write full descriptive tests for complex business logic.

Comprehensive model tests are your first line of defence. They run in seconds, require no browser, and tell you immediately when business logic breaks.

Leave a Comment

Your email address will not be published. Required fields are marked *