How to Implement TDD in Ruby on Rails: A Complete Guide
Why We Need TDD?
- Write only the necessary code by testing first.
- Catch bugs early during development.
- Code becomes modular and easier to maintain.
- Tests act as self-documentation.
- Confidence in making changes and refactoring.
TDD Workflow in Rails
- Install RSpec with:bundle add rspec-rails –group “development, test”rails generate rspec:install
- Write a failing test.
- Write minimal code to pass the test.
- Refactor the code while keeping the test green.
- Repeat the cycle for new features.
From Requirement to Code: A TDD Scenario
đ Scenario: Build a Simple Task Manager
Letâs walk through how TDD works in real-world Rails development. Imagine you are building a Task Manager application where users can:
- Create a task with a title and deadline
- Mark tasks as complete
- Prevent tasks from having past deadlines
â Step 1: Understand Requirements
- Title must be present and at least 5 characters
- Deadline must be today or a future date
- Users can toggle task status between complete/incomplete
đ§Ș Step 2: Write Failing Tests
describe Task do
it "is invalid without a title" do
task = Task.new(title: nil)
expect(task).not_to be_valid
end
it "is invalid if title is too short" do
task = Task.new(title: "Do")
expect(task).not_to be_valid
end
it "is invalid with past deadline" do
task = Task.new(title: "Plan", deadline: Date.yesterday)
expect(task).not_to be_valid
end
end
đ» Step 3: Implement Minimal Code
Now add validations in the Task
model to make those tests pass:
class Task < ApplicationRecord
validates :title, presence: true, length: { minimum: 5 }
validate :deadline_cannot_be_in_the_past
def deadline_cannot_be_in_the_past
if deadline.present? && deadline < Date.today
errors.add(:deadline, "can't be in the past")
end
end
end
đ Step 4: Add Feature Test for Toggling Completion
Then write another test for toggling status:
it "can toggle completion status" do
task = Task.create!(title: "Write Blog", deadline: Date.tomorrow)
expect(task.complete).to eq(false)
task.toggle!(:complete)
expect(task.complete).to eq(true)
end
đ Step 5: Refactor and Repeat
You now have a working, tested feature. Continue this cycle for every new requirement:
- Write a failing test
- Write minimum code to pass
- Refactor while keeping the test green
This method keeps development focused, prevents regressions, and builds a reliable Rails app feature by feature.
Case Study: Using TDD to Build a Secure Login Flow in Rails
đ Background
A SaaS startup was building a new Ruby on Rails application with user accounts, including a login and session management system. Security and correctness were top priorities, especially for handling failed login attempts, invalid sessions, and password hashing.
đŻ Why TDD?
- Authentication logic has many edge cases (blank input, wrong password, inactive account, etc.)
- Security-critical feature â mistakes can lead to data breaches
- CI/CD pipeline needed tests to prevent broken auth from deploying
â Requirements
- User can log in using email and password
- Passwords must be securely stored (not plain text)
- Invalid credentials should show a proper error message
- Session should persist the user across requests
- Provide logout functionality that ends the session
đ§Ș Step 1: Write Failing Tests First (TDD)
RSpec request and model specs were created before any controller code.
describe "POST /login" do
let!(:user) { User.create(email: "[email protected]", password: "password123") }
it "logs in with valid credentials" do
post "/login", params: { email: "[email protected]", password: "password123" }
expect(response).to have_http_status(:found)
expect(session[:user_id]).to eq(user.id)
end
it "rejects invalid credentials" do
post "/login", params: { email: "[email protected]", password: "wrong" }
expect(response).to have_http_status(:unauthorized)
end
end
đ» Step 2: Write Just Enough Code to Pass
Basic login logic was implemented in a `SessionsController`:
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to dashboard_path
else
render json: { error: "Invalid credentials" }, status: :unauthorized
end
end
end
And the User model used `has_secure_password`:
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
end
đ Step 3: Refactor & Expand Coverage
- Added flash messages and integration tests with Capybara
- Tested logout flow and redirection
- Tested session expiration and invalid session access
âïž CI/CD Integration
The team used GitHub Actions to run the test suite on every pull request:
- All login specs were green before merging
- Bug: One PR had accidentally removed session clearing during logout â tests caught it
- New developer onboarded and understood auth flow by reading test files
đ Outcome
- 100% test coverage for login-related code
- Zero login-related production bugs in first 3 months
- Easy refactoring of authentication logic later to support OAuth, without regressions
- Peace of mind for developers pushing auth updates
đĄ Key Takeaways
- Write request specs first for auth â they mimic the real user experience
- Always test failure cases: wrong password, empty fields, expired sessions
- TDD makes authentication not only more secure, but easier to evolve
Case Study: TDD in a Rails API for a Fintech Startup
đ Project Background
A fintech startup was building a Rails API to handle loan applications and credit scoring. The app required strict validation, secure processing, and minimal downtime.
đ Why TDD Was Chosen
- Financial data needs high confidence in logic
- Many edge cases (e.g. missing income, invalid amounts)
- Multiple devs working on features in parallel
- Need for CI/CD pipelines with green test gates
đ§Ș TDD Workflow Example: Loan Approval Logic
Requirement: A loan should only be approved if:
- The applicant is over 21
- Monthly income is more than 3x the requested EMI
describe LoanApplication do
it "rejects if applicant is underage" do
loan = LoanApplication.new(age: 19)
expect(loan).not_to be_approved
end
it "approves if income is sufficient" do
loan = LoanApplication.new(age: 25, income: 60000, emi: 15000)
expect(loan.approve!).to eq("approved")
end
end
đ» Model Logic Based on Tests
class LoanApplication < ApplicationRecord
def approved?
age >= 21 && income.to_i >= emi.to_i * 3
end
def approve!
approved? ? "approved" : "rejected"
end
end
â Results of Using TDD
- Over 95% test coverage across models and API endpoints
- Zero production crashes in the first six months post-launch
- Developers onboarded faster by reading test specs
- Confident deploys through GitHub Actions with full test runs
This startup scaled from 1 to 10 engineers while maintaining code quality thanks to a strong TDD foundation in their Rails backend.
TDD Interview Questions and Answers with Examples
- Q1. What is TDD and why is it important in Rails?
A: TDD (Test-Driven Development) is a software development approach where you write tests before writing the actual code. In Rails, TDD ensures clean, bug-free, and maintainable code.
Example:
Write a test to validate email presence in a User model:it "is invalid without an email" do
user = User.new(email: nil)
expect(user).not_to be_valid
end - Q2. What are the main steps in a TDD cycle?
A: Red â Green â Refactor.- Red: Write a failing test.
- Green: Write code to make the test pass.
- Refactor: Improve the code while keeping tests green.
- Q3. What tools do you use for TDD in Rails?
A: Common tools include:- RSpec â for writing tests
- FactoryBot â for test data setup
- Shoulda Matchers â for common validations
- Capybara â for integration/system tests
- Q4. How do you test associations in models?
A: Using RSpec and shoulda-matchers:describe User do
it { should have_many(:posts) }
end - Q5. How do you test controller actions in TDD?
A: Using request specs or controller specs (deprecated):describe "GET /posts" do
it "returns http success" do
get "/posts"
expect(response).to have_http_status(:success)
end
end - Q6. Whatâs the difference between unit, integration, and system tests?
A:- Unit tests: Test one model or method in isolation
- Integration tests: Test how multiple parts (e.g., controllers + models) work together
- System tests: Simulate user behavior via UI (Capybara)
- Q7. How can TDD help with refactoring?
A: With tests in place, you can safely refactor code. If tests still pass after refactoring, your changes didnât break functionality.
Example: You refactor `full_name` method:def full_name
[first_name, last_name].compact.join(" ")
end - Q8. Should you test private methods?
A: No, you should only test public behavior. If private logic needs tests, refactor it into a separate class. - Q9. What are some signs of poorly written tests?
A:- Too tightly coupled to implementation
- Hard to understand or redundant
- Break with small code changes
- Donât test edge cases or business rules
- Q10. Whatâs the biggest challenge in using TDD?
A: The initial learning curve and discipline to write tests first. Also, knowing what to test and what to skip takes experience.
Best Practices for TDD in Rails
- 1. Write tests before writing production code
This ensures you only write the code that is needed to fulfill a requirement. - 2. Keep your tests fast and focused
Unit tests should run quickly. If a test takes too long, isolate external dependencies. - 3. Use FactoryBot for clean test data
Avoid manually setting attributes every time. Define factories to reduce duplication.FactoryBot.define do
factory :user do
email { "[email protected]" }
password { "password123" }
end
end - 4. Start with model tests
Test validations, associations, and custom logic in models before jumping to controllers or views. - 5. Cover edge cases and failure scenarios
Donât just test happy paths. Test invalid inputs, exceptions, and unexpected behavior. - 6. Use descriptive test names
Clear test names act as documentation for other developers.
it "returns full name of user" do ... end
- 7. Group related tests with
describe
andcontext
Structure your test files for readability and logical separation.describe User do
context "when email is missing" do
it "is invalid" do
...
end
end
end - 8. Use `let` and `subject` properly
These help reduce duplication and make your specs cleaner. - 9. Donât test implementation details
Focus on public behavior and outcomes â not private methods or internal logic. - 10. Integrate with CI pipelines
Make your test suite run on every pull request using tools like GitHub Actions or GitLab CI.
TDD vs BDD: Definitions, Differences, and Pros & Cons
đ Definitions
- Test-Driven Development (TDD): Write tests before implementation, focusing on internal code correctness.
- Behavior-Driven Development (BDD): Write scenarios describing the behavior of the application from the end user’s perspective, often using plain language.
âïž TDD vs BDD: Key Differences
Aspect | TDD | BDD |
---|---|---|
Focus | Code correctness | User behavior |
Audience | Developers | Developers, testers, business stakeholders |
Syntax | RSpec, Minitest | Cucumber, RSpec (with feature syntax) |
Language | Technical (e.g., `expect(model).to be_valid`) | Plain English (e.g., Given/When/Then) |
Test Type | Unit, integration | Acceptance, feature, end-to-end |
â Pros and â Cons
Approach | Pros | Cons |
---|---|---|
TDD |
|
|
BDD |
|
|
đ When to Use What?
- Use TDD for: APIs, service layers, libraries, model logic, performance-critical code
- Use BDD for: User flows, stakeholder collaboration, cross-functional teams, product discovery
- Use both together: BDD defines *what* should happen, TDD defines *how* itâs implemented
TDD: When to Use It and When You Might Skip It
â Scenarios Where You Should Use TDD
- 1. Financial Transactions (e.g., eCommerce checkout, payment gateway):
Bugs here can cause real money loss. TDD helps prevent logic errors in discounts, taxes, or totals.it "applies coupon discount correctly" do
order = Order.new(subtotal: 100, coupon: Coupon.new(percent_off: 10))
expect(order.total_price).to eq(90)
end - 2. APIs with Business Rules:
TDD ensures request/response behavior matches requirements, especially for authentication or data validation.it "rejects signup if email is missing" do
post "/api/signup", params: { password: "secret" }
expect(response.status).to eq(422)
end - 3. Core Models with Validations and Relations:
TDD is great for models likeUser
,Invoice
,Task
, etc. - 4. Long-Term or Team Projects:
When multiple developers are working together, TDD ensures safety and clarity. It protects against regressions. - 5. Background Jobs (e.g., sending emails, syncing data):
You want to ensure jobs perform correctly and handle retries or failures gracefully.
â Scenarios Where TDD Might Be Skipped
- 1. Throwaway Code or Prototypes:
Youâre building a demo or one-time feature that may never ship. Speed is prioritized over stability. - 2. Static Pages or Simple HTML Views:
A Terms & Conditions page or FAQ with no logic â no need for tests unless you have a CMS-driven setup. - 3. Complex Frontend Layouts (Visual-Only):
If you’re designing a layout with Tailwind/Bootstrap and no data logic, snapshot or visual testing is better than TDD. - 4. Spike Solutions / Exploratory Code:
Youâre exploring an idea (like trying a new gem). You can add tests later once the idea solidifies. - 5. CRUD Scaffolds That Will Be Replaced:
When using Rails generators just to build early views, itâs okay to delay tests until real logic is added.
Tip: Even if you skip TDD for now, write at least model specs
and request specs
for key logic later. You can layer tests progressively.
Alternatives to TDD
- BDD: Behavior Driven Development â focus on user behavior.
- ATDD: Acceptance Test Driven Development â focus on business rules.
- Manual Testing: Practical but risky without automation.
Real-World Case Study
Basecamp by 37signals
Basecamp uses TDD throughout their Rails product lifecycle. They maintain a large RSpec test suite and rely on CI to catch regressions before deploys.
- Thousands of model and request specs
- Team collaboration improves through test clarity
- Tests run automatically on GitHub Actions for every pull request
Learn more about Rails Testing Framework setup
nu9tuw
k5r9f9
Start earning passive incomeâbecome our affiliate partner! https://shorturl.fm/3xSkx
Turn traffic into cashâapply to our affiliate program today! https://shorturl.fm/jWvVC
Become our partner now and start turning referrals into revenue! https://shorturl.fm/ed1dT
Boost your profits with our affiliate programâapply today! https://shorturl.fm/HywAF
Share our link, earn real moneyâsignup for our affiliate program! https://shorturl.fm/Tz5Cj
Turn your traffic into cashâjoin our affiliate program! https://shorturl.fm/tbM5r
Start earning on every saleâbecome our affiliate partner today! https://shorturl.fm/7eK8l
Join our affiliate program and start earning commissions todayâsign up now! https://shorturl.fm/tPvDj
Join our affiliate community and maximize your profitsâsign up now! https://shorturl.fm/BOaJw
Promote our brand and watch your income growâjoin today! https://shorturl.fm/qdKwV
Turn traffic into cashâapply to our affiliate program today! https://shorturl.fm/ksiXF
Tap into unlimited earningsâsign up for our affiliate program! https://shorturl.fm/ROjUt
Promote our brand, reap the rewardsâapply to our affiliate program today! https://shorturl.fm/hCyeZ
Share your unique link and earn up to 40% commission! https://shorturl.fm/kJtS9
Apply now and receive dedicated support for affiliates! https://shorturl.fm/Xtb2n
Get rewarded for every recommendationâjoin our affiliate network! https://shorturl.fm/9CsQK
Get rewarded for every recommendationâjoin our affiliate network! https://shorturl.fm/9CsQK
Unlock top-tier commissionsâbecome our affiliate partner now! https://shorturl.fm/pMnoo
Unlock exclusive rewards with every referralâenroll now! https://shorturl.fm/1H5I3
Start profiting from your trafficâsign up today! https://shorturl.fm/me4JY
Tap into unlimited earning potentialâbecome our affiliate partner! https://shorturl.fm/uw1da
Refer friends, collect commissionsâsign up now! https://shorturl.fm/fmnEx
Join forces with us and profit from every click! https://shorturl.fm/tvLMj
Join our affiliate program and start earning commissions todayâsign up now! https://shorturl.fm/ROl8N
Join our affiliate program and start earning commissions todayâsign up now! https://shorturl.fm/wOYcA
Turn your audience into earningsâbecome an affiliate partner today! https://shorturl.fm/kJ7mg
Get paid for every clickâjoin our affiliate network now! https://shorturl.fm/KpH1x
Apply now and receive dedicated support for affiliates! https://shorturl.fm/3q5vE
Apply now and receive dedicated support for affiliates! https://shorturl.fm/KUIlq