Testing with RSpec
Reviewed & published by Brayan K
RSpec is Ruby's most popular behavior-driven testing framework, letting you describe what your code should do in clear, readable specs and prove it with expectations and matchers.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll structure specs with describe/context/it, assert with matchers, share setup using let and hooks, and isolate code with mocks and stubs.
What You'll Learn in This Lesson
- Structuring specs with describe, context, it
- Expectations and matchers (eq, be, include)
- Lazy helpers with let and subject
- Setup and teardown with before/after hooks
- Mocks and stubs with double, allow, receive
- Model and request specs in Rails
1️⃣ describe, context, it, and Matchers
describe names the thing under test, context groups examples by situation, and it holds one example. Inside, expect(actual).to matcher states what should be true.
# RSpec describes BEHAVIOR in plain, readable blocks.
# A spec file usually ends in _spec.rb, e.g. calculator_spec.rb
class Calculator
def add(a, b); a + b; end
def divide(a, b); a / b; end
end
RSpec.describe Calculator do # the thing under test
it "adds two numbers" do # one example (test case)
calc = Calculator.new
expect(calc.add(2, 3)).to eq(5) # an expectation with a matcher
end
context "when dividing by zero" do # a situation/state
it "raises an error" do
calc = Calculator.new
expect { calc.divide(1, 0) }.to raise_error(ZeroDivisionError)
end
end
end
# Run it with: rspec calculator_spec.rbRSpec ships with a rich set of matchers. let and subject keep specs concise.
# Common matchers — expect(actual).to MATCHER
expect(2 + 2).to eq(4) # value equality (==)
expect(nil).to be_nil # identity / predicate matchers
expect([1, 2, 3]).to include(2) # collection membership
expect("hello").to start_with("he") # string matcher
expect(5).to be > 3 # comparison
expect(user).to be_admin # calls user.admin? (predicate)
expect { risky }.to raise_error # block form for exceptions
# let and subject keep specs DRY:
RSpec.describe Array do
subject { [1, 2, 3] } # the implicit object under test
let(:total) { subject.sum } # lazy, memoized helper
it "sums its elements" do
expect(total).to eq(6)
end
end2️⃣ Hooks, Mocks, and Stubs
before/after hooks run shared setup and teardown. A double stands in for a real object; allow stubs a return value, while expect(...).to receive mocks a required call.
# Mocks and stubs isolate the code under test from collaborators.
# A STUB makes a method return a canned value (no real work):
weather = double("WeatherApi")
allow(weather).to receive(:temperature).and_return(21)
expect(weather.temperature).to eq(21)
# A MOCK also sets an expectation that a message IS received:
mailer = double("Mailer")
expect(mailer).to receive(:send_welcome).with("ada@example.com")
mailer.send_welcome("ada@example.com") # satisfies the expectation
# Hooks share setup/teardown across examples:
RSpec.describe "hooks" do
before(:each) { @items = [] } # runs before every example
after(:each) { @items.clear } # runs after every example
it "starts empty" do
expect(@items).to be_empty
end
endYour turn. Complete the expectation so the greeter spec passes, then run it.
# 🎯 YOUR TURN — finish the spec, then run: rspec greeter_spec.rb
class Greeter
def greet(name)
"Hello, #{name}!"
end
end
RSpec.describe Greeter do
let(:greeter) { Greeter.new } # lazy, memoized helper
it "greets a person by name" do
# 👉 TODO: expect greeter.greet("Ada") to eq "Hello, Ada!"
expect(greeter.greet("Ada")).to eq("Hello, Ada!")
end
end
# ✅ Expected output:
# .
# 1 example, 0 failuresMini-Challenge: Spec a Stack
Write a small Stack class and cover it with two examples using let and matchers. Run with rspec stack_spec.rb.
# 🎯 MINI-CHALLENGE: spec a small class.
# Save as stack_spec.rb and run with: rspec stack_spec.rb
#
# 1. Write a Stack class with push and pop.
# 2. Use describe Stack and a let(:stack) helper.
# 3. Add examples: it "is empty when created", it "pops the last item".
# 4. Use matchers like be_empty and eq.
#
# ✅ Example output:
# ..
# 2 examples, 0 failures
# your code hereCommon Errors (and the fix)
- Using eq for exceptions — Wrap risky code in a block: expect { ... }.to raise_error.
- Forgetting the block with let — It needs a block: let(:x) { 5 }, then reference x.
- be vs eq — be checks identity (same object); eq checks value equality.
- Over-mocking — Mock too much and tests pass while real code breaks; mock only true collaborators.
- Wrong file name — Spec files should end in _spec.rb so the runner finds them.
Pro Tips
- 💡 Use a .rspec file: add --format documentation for readable output.
- 💡 Shared examples: shared_examples + it_behaves_like remove duplication.
- 💡 spec_helper: put common config in spec/spec_helper.rb and require it.
📋 Quick Reference — RSpec
| Concept | RSpec Syntax |
|---|---|
| Group | RSpec.describe Thing do |
| Example | it "does x" do ... end |
| Expectation | expect(x).to eq(5) |
| Helper | let(:x) { ... } |
| Stub | allow(o).to receive(:m) |
| Mock | expect(o).to receive(:m) |
🎉 Lesson Complete!
- ✅ describe/context/it structure your specs
- ✅ expect(...).to matcher states expectations
- ✅ let and subject keep specs DRY
- ✅ before/after hooks share setup
- ✅ double, allow, and receive isolate code
Practice quiz
What is RSpec?
- a behavior-driven testing framework for Ruby
- a database adapter
- a deployment tool
- a Ruby web framework
Answer: a behavior-driven testing framework for Ruby. RSpec is a popular behavior-driven development (BDD) testing framework for Ruby.
Which RSpec block declares an individual example (test case)?
- context
- subject
- it
- describe
Answer: it. An it block holds a single example, the smallest unit of an RSpec test.
What does describe usually name?
- a gem dependency
- the thing under test, such as a class or method
- a single assertion
- a test database
Answer: the thing under test, such as a class or method. describe groups examples for a particular class, method, or feature being tested.
What is the main purpose of a context block?
- to define matchers
- to run setup code only
- to install gems
- to group examples that share a particular situation or state
Answer: to group examples that share a particular situation or state. context groups examples under a specific condition, like when logged in, improving readability.
Which expectation checks value equality with eq?
- expect(x).to eq(5)
- x.should_equal 5
- expect(x).is(5)
- assert_equal(5, x)
Answer: expect(x).to eq(5). RSpec uses expect(x).to eq(5); eq tests value equality using ==.
What does let provide in RSpec?
- a globally shared variable
- a lazily evaluated, memoized helper available in examples
- a way to skip tests
- a database migration
Answer: a lazily evaluated, memoized helper available in examples. let defines a helper that is computed lazily on first use and memoized within an example.
When does a before(:each) hook run?
- once for the whole file
- only after all examples
- never, it is deprecated
- before each example in the group
Answer: before each example in the group. before(:each) runs its setup code before every example in the group.
What does double create in RSpec?
- a duplicate test file
- a second expectation
- a test double (mock object) standing in for a real one
- a real object from the database
Answer: a test double (mock object) standing in for a real one. double creates a lightweight test double you can stub or set message expectations on.
Which sets up a stub so a method returns a canned value?
- double.return
- allow(obj).to receive(:name).and_return("Ada")
- expect(obj).to be_name
- obj.stub_all
Answer: allow(obj).to receive(:name).and_return("Ada"). allow(obj).to receive(:name).and_return(value) stubs the method to return that value.
In a Rails app, what do request specs primarily test?
- HTTP behavior end to end through routes and controllers
- CSS styling
- database backups
- gem versions
Answer: HTTP behavior end to end through routes and controllers. Request specs exercise the full HTTP stack, from route to controller to response.
Continue this course
- Previous: Capstone: A Rails Blog App
- Next: Sinatra & Rack — The Rack interface every Ruby web app runs on, and the Sinatra micro-framework
- Quick reference: Ruby cheat sheet
Frequently asked questions
What is RSpec and how is it different from Minitest?
RSpec is a behavior-driven development (BDD) testing framework for Ruby. You describe what code should do using readable blocks like describe, context, and it, then state expectations with expect(...).to matcher. Minitest, which ships with Ruby, is lighter and uses a more classic assertion style (assert_equal, assert) plus an optional spec-style DSL. Both are excellent and widely used. RSpec offers a richer matcher and mocking library and a very expressive syntax; Minitest is minimal, fast, and dependency-free. Many Rails projects use RSpec, while plenty of gems and apps happily use Minitest.
What are let and subject for?
Both keep specs DRY and readable. let(:name) { ... } defines a helper that is evaluated lazily — only when first referenced in an example — and then memoized for the rest of that example, so repeated calls return the same object. subject names the primary object under test; you can reference it directly or let RSpec use it implicitly with one-liner expectations. Because let is lazy, setup that an example does not touch never runs, which keeps tests fast and focused. Use let for collaborators and inputs, and subject for the single thing each describe block is about.
What is the difference between a mock and a stub?
A stub replaces a method so it returns a value you control, letting you isolate the code under test from slow or unpredictable collaborators — for example allow(api).to receive(:fetch).and_return(data). A mock goes further: it also sets an expectation that the message will actually be received, and the example fails if it is not, as in expect(mailer).to receive(:deliver). In RSpec you create test doubles with double, then stub them with allow or mock them with expect. The rule of thumb: stub queries (you only need a return value) and mock commands (you care that the call happened).
How do model and request specs differ in Rails?
Model specs test a single model in isolation — validations, associations, scopes, and business methods — by creating records and asserting on their behavior, so they stay close to the database layer. Request specs test the application end to end over HTTP: you make a get or post to a path and assert on the response status, body, or redirects, exercising routing, the controller, and the rendered result together. Model specs give fast, granular coverage of logic, while request specs verify that the pieces wire up correctly for a real request. A healthy suite uses both, plus shared examples to avoid repeating common expectations.