RSpec Tutorial: Getting Started with Ruby Testing
RSpec is the most widely used testing framework for Ruby. Its expressive syntax reads almost like natural language, which makes test suites easier to understand and maintain. This tutorial covers the core building blocks: describe, context, it, expect, and matchers.
Installing RSpec
Add RSpec to your Gemfile:
group :test do
gem 'rspec'
endRun bundle install, then initialize RSpec in your project:
bundle exec rspec --initThis creates .rspec (configuration file) and spec/spec_helper.rb.
For a standalone Ruby project, you can also install globally: gem install rspec.
Your First Spec
Create a file spec/calculator_spec.rb:
require 'spec_helper'
require_relative '../calculator'
RSpec.describe Calculator do
describe '#add' do
it 'returns the sum of two numbers' do
calc = Calculator.new
expect(calc.add(2, 3)).to eq(5)
end
end
endAnd the implementation calculator.rb:
class Calculator
def add(a, b)
a + b
end
endRun it:
bundle exec rspec spec/calculator_spec.rbThe Core Structure
describe
describe groups related specs. You typically pass a class or a method name:
RSpec.describe Calculator do
# specs for Calculator class
endNested describe blocks let you group by method:
RSpec.describe Calculator do
describe '#add' do
# specs for the add method
end
describe '#subtract' do
# specs for the subtract method
end
endBy convention, prefix instance methods with # and class methods with ..
context
context is an alias for describe, but it reads better when describing conditions:
RSpec.describe Calculator do
describe '#divide' do
context 'when divisor is zero' do
it 'raises a ZeroDivisionError' do
calc = Calculator.new
expect { calc.divide(10, 0) }.to raise_error(ZeroDivisionError)
end
end
context 'when divisor is non-zero' do
it 'returns the correct quotient' do
calc = Calculator.new
expect(calc.divide(10, 2)).to eq(5)
end
end
end
endGood context names start with "when" or "with" to describe a state.
it
it defines a single example (a test). The string after it describes what the code should do:
it 'returns true when the user is active' do
# test code
endKeep it blocks focused on one behavior. If you have multiple unrelated assertions in one it, split them.
expect
expect is the assertion mechanism. The basic form:
expect(actual).to matcher
expect(actual).not_to matcherCore Matchers
RSpec ships with a rich set of matchers.
Equality:
expect(result).to eq(5) # value equality
expect(result).to be(object) # object identity (same reference)
expect(result).to eql(5.0) # type-sensitive equalityTruthiness:
expect(result).to be_truthy
expect(result).to be_falsy
expect(result).to be_nil
expect(result).not_to be_nilComparisons:
expect(score).to be > 90
expect(score).to be >= 80
expect(score).to be_between(1, 100).inclusiveStrings:
expect(message).to include('error')
expect(message).to start_with('Error:')
expect(message).to end_with('!')
expect(message).to match(/^\d{3}-\d{4}$/)Collections:
expect(array).to include(3)
expect(array).to contain_exactly(1, 2, 3) # order-independent
expect(array).to match_array([3, 1, 2]) # same as contain_exactly
expect(array).to have_attributes(length: 3)Exceptions:
expect { risky_method }.to raise_error(RuntimeError)
expect { risky_method }.to raise_error(RuntimeError, 'message')
expect { risky_method }.not_to raise_errorPredicate matchers:
RSpec auto-generates matchers from predicate methods. If an object has a valid? method:
expect(user).to be_valid
expect(user).not_to be_validAny method ending in ? works this way: be_empty, be_frozen, be_nil, etc.
let and let!
Avoid repeating setup code with let:
RSpec.describe User do
let(:user) { User.new(name: 'Alice', email: 'alice@example.com') }
it 'has a name' do
expect(user.name).to eq('Alice')
end
it 'has an email' do
expect(user.email).to eq('alice@example.com')
end
endlet is lazy — the block runs only when user is first referenced. let! is eager — runs before each example:
let!(:user) { User.create!(name: 'Alice') }Use let! when you need the side effect (like a database record) to exist even if the example doesn't reference the variable directly.
before and after Hooks
before runs setup code before each example:
RSpec.describe Order do
before(:each) do
@order = Order.new
@order.add_item(Item.new(price: 10))
end
it 'calculates total' do
expect(@order.total).to eq(10)
end
endbefore(:all) (or before(:context)) runs once per describe block — use sparingly as it can cause test pollution.
after is the cleanup counterpart:
after(:each) do
# cleanup
endsubject
subject defines the object under test. RSpec auto-sets it to an instance of the described class:
RSpec.describe Calculator do
# subject is automatically Calculator.new
it { is_expected.to respond_to(:add) }
endYou can override subject explicitly:
RSpec.describe User do
subject { User.new(name: 'Bob', active: true) }
it { is_expected.to be_active }
endRunning RSpec
Run all specs:
bundle exec rspecRun a specific file:
bundle exec rspec spec/calculator_spec.rbRun a specific line:
bundle exec rspec spec/calculator_spec.rb:15Run with documentation format:
bundle exec rspec --format documentation.rspec Configuration
Your .rspec file sets default flags:
--require spec_helper
--format documentation
--colorOrganizing Specs
Mirror your lib/ or app/ structure in spec/:
lib/
calculator.rb
user.rb
spec/
calculator_spec.rb
user_spec.rb
spec_helper.rbIn Rails, use spec/models/, spec/controllers/, spec/requests/, etc.
Best Practices
Name your examples clearly. The output of rspec --format documentation should read like a specification document.
One assertion per example. Focused tests are easier to debug when they fail.
Use context for branching. Each branch in your code (if/else, raise/return) deserves its own context.
Avoid before(:all). Shared mutable state across examples is a source of flaky tests.
Don't over-mock. Test real behavior where possible. Mock external services and I/O, not your own code.
What's Next
Once you're comfortable with the basics, explore:
- Mocking and stubbing with
double,allow, andexpect - Shared examples with
shared_examples_for - Custom matchers for domain-specific assertions
- RSpec Rails for model, request, and system specs
RSpec's design philosophy — readable specs that serve as documentation — pays off as your project grows. Small investment in test clarity upfront, large dividends when something breaks.