
Minitest ships with Rails. It's fast, lightweight, uses plain Ruby assertion syntax, and the core Rails team uses it daily. For many teams it's more than enough. Still, a large slice of the community reaches for RSpec, and it's worth understanding why.
RSpec is a Behavior-Driven Development framework built around an expressive DSL that reads almost like English. This is where the "tests as documentation" idea really earns its keep. You're not just asserting that a == b — you're describing how a piece of your system is supposed to behave.
# An example of RSpec's expressive DSL
RSpec.describe OrderProcessor do
context "when the user has sufficient funds" do
it "processes the transaction successfully" do
# setup, action, assertion
end
end
end
Most production Rails shops pair RSpec with a few near-standard companions: FactoryBot for test data, Shoulda Matchers for one-line validation and association specs, VCR or WebMock for stubbing third-party APIs, and Capybara for system specs that drive a real browser. Together they form the de facto SaaS testing stack.
Because Rails leans heavily on MVC, TDD also nudges you toward cleaner separation of concerns. The friction of testing fat models and fat controllers tends to push logic out into Service Objects, POROs (Plain Old Ruby Objects), Form Objects, or Concerns — exactly where it belongs once your app grows past the prototype stage.
TDD's core loop is a tight micro-iteration: Red, Green, Refactor. Here's what that looks like building a feature in Rails.
You write the test before any implementation. This forces you to think about the interface of your object before you worry about its internals.
Say we're building a User model that needs age verification — a common requirement for SaaS products with regulatory or content restrictions.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
describe "validations" do
it "is invalid when the user is under 18" do
user = User.new(date_of_birth: 15.years.ago)
user.valid?
expect(user.errors[:date_of_birth])
.to include("you must be at least 18 years old")
end
end
end
Run bundle exec rspec spec/models/user_spec.rb. It fails. That's the Red, and it's the point — you've just proven the test can detect the absence of the behavior.
Your only job now is to get to green. Don't optimize, don't generalize, don't reach for the elegant abstraction. Write the smallest thing that works.
# app/models/user.rb
class User < ApplicationRecord
validate :must_be_eighteen
def must_be_eighteen
return if date_of_birth.blank?
if date_of_birth > 18.years.ago
errors.add(:date_of_birth, "you must be at least 18 years old")
end
end
end
Run the suite. Green.
With a passing test as your safety net, you're free to improve the design. Maybe you extract the rule into a custom validator so other models can reuse it. Maybe you just tighten the syntax for clarity.
# app/models/user.rb
class User < ApplicationRecord
validates :date_of_birth, presence: true
validate :user_meets_minimum_age
private
def user_meets_minimum_age
return if date_of_birth.blank?
return unless under_age?
errors.add(:date_of_birth, "you must be at least 18 years old")
end
def under_age?
date_of_birth > 18.years.ago
end
end
If you break something, RSpec tells you immediately. That's the whole bargain: tests buy you the freedom to refactor aggressively.
Years ago, David Heinemeier Hansson — Rails' creator — sparked a memorable debate by declaring "TDD is dead." His point was that dogmatically unit-testing every method leads to brittle tests and design damage: mocks piled on mocks, indirection for its own sake.
That critique still stands. But pragmatic TDD, as opposed to the religious version, remains enormously valuable for SaaS teams shipping continuously:
Rails won't force you into TDD. You can scaffold a resource and start piling logic into a controller this afternoon. For a weekend project, that's fine.
For a SaaS product that needs to survive its second year, its third engineer, and its fifth Rails upgrade, the Red–Green–Refactor cycle stops looking like overhead and starts looking like leverage. Treat tests as a design tool rather than an end-of-sprint chore, and they'll quietly shape a codebase you actually want to keep working in.
For a SaaS team, that shift isn't just an engineering nicety. It's what makes weekly releases boring, on-call rotations quiet, and the next big refactor actually possible.

Ryan previously served as a PCI Professional Forensic Investigator (PFI) of record for 3 of the top 10 largest data breaches in history. With over two decades of experience in cybersecurity, digital forensics, and executive leadership, he has served Fortune 500 companies and government agencies worldwide.

Hallucinations are not a prompt problem—they are an architecture problem. Here is how wrapping LLM calls in a Finite-State Machine transforms a probabilistic model into a predictable, inspectable software component you can ship to paying customers.

How Apple Intelligence hallucinations exposed fragile market microstructure, and why iOS 26's Liquid Glass UI and FinanceKit API are fundamentally reshaping fintech data provenance, algorithmic trading, and the death of screen scraping.

A deep technical analysis of Notion's architectural security gaps, permission model failures, AI exfiltration vulnerabilities, and why enterprise IT leaders should look past the polished UI before adopting it as a system of record.