Testing with RSpec

· 4 min read · Updated March 7, 2026 · intermediate
rspec testing bdd tdd mocking

RSpec is Ruby’s most popular testing framework. It uses a Behavior-Driven Development (BDD) style that makes your tests read like natural language specifications. In this guide, you’ll learn how to write clean, maintainable tests that give you confidence in your code.

Setting Up RSpec

First, add RSpec to your Gemfile:

group :test do
  gem 'rspec', '~> 3.12'
end

Then run bundle install and initialize RSpec:

bundle exec rspec --init

This creates a .rspec file and a spec directory where your tests will live.

Your First Test

RSpec organizes tests around two core concepts: describe blocks (which group related tests) and it blocks (individual test cases). Let’s test a simple calculator class:

# spec/calculator_spec.rb
RSpec.describe Calculator do
  describe '#add' do
    it 'returns the sum of two numbers' do
      calculator = Calculator.new
      expect(calculator.add(2, 3)).to eq(5)
    end
  end
end

The describe method takes a class name or string description. The it method defines a single test case. The expect syntax reads naturally: “expect the result to equal 5.”

Matchers

Matchers are RSpec’s way of making assertions readable. Here are the most common ones:

Equality Matchers

expect(actual).to eq(expected)      # value equality (==)
expect(actual).to eql(expected)    # deep equality
expect(actual).to be(expected)     # same object (equal?)
expect(actual).to match(/pattern/) # regex match

Truthiness Matchers

expect(value).to be_truthy   # not nil or false
expect(value).to be_falsey  # nil or false
expect(value).to be_nil     # nil
expect(value).to be true    # exact boolean

Collection Matchers

expect(array).to include(item)
expect(array).to contain_exactly(item1, item2)
expect(hash).to have_key(:key)
expect(hash).to have_value(value)

Predicate Matchers

expect(collection).to be_empty
expect(number).to be > 5
expect(object).to be_instance_of(Class)
expect(object).to be_a(Class)

Before and After Hooks

Hooks let you run code before or after each test:

RSpec.describe User do
  before(:each) do
    @user = User.create(name: 'Alice')
  end

  after(:each) do
    User.delete_all
  end

  describe '#greet' do
    it 'greets the user by name' do
      expect(@user.greet).to include('Alice')
    end
  end
end

The :each symbol runs the hook for every test. Use :all to run once for the entire describe block:

before(:all) do
  DatabaseCleaner.start
end

after(:all) do
  DatabaseCleaner.clean
end

Let and Subject

The let method defines memoized helpers that lazy-load only when needed:

RSpec.describe Order do
  let(:customer) { Customer.new(name: 'Bob') }
  let(:order) { Order.new(customer: customer) }

  it 'belongs to a customer' do
    expect(order.customer).to eq(customer)
  end

  it 'calculates total correctly' do
    order.add_item(Item.new(price: 10))
    order.add_item(Item.new(price: 20))
    expect(order.total).to eq(30)
  end
end

let is lazy—it won’t be evaluated until you use it. This makes tests faster and more focused.

Use subject when the main object being tested is used repeatedly:

RSpec.describe Calculator do
  subject { Calculator.new }

  it 'adds two numbers' do
    expect(subject.add(2, 3)).to eq(5)
  end

  it 'subtracts two numbers' do
    expect(subject.subtract(5, 3)).to eq(2)
  end
end

Testing Exceptions

Use raise_error to test that code raises exceptions:

RSpec.describe Calculator do
  describe '#divide' do
    it 'raises an error when dividing by zero' do
      expect { subject.divide(10, 0) }.to raise_error(ZeroDivisionError)
    end

    it 'accepts custom error messages' do
      expect { subject.divide(10, 0) }.to raise_error(/division/)
    end
  end
end

Mocking and Stubbing

Mocks replace real objects with test doubles:

RSpec.describe OrderProcessor do
  let(:payment_gateway) { double('PaymentGateway') }
  let(:processor) { OrderProcessor.new(payment_gateway) }

  it 'charges the customer via the payment gateway' do
    expect(payment_gateway).to receive(:charge).with(100)
    processor.process(order_with_total_of(100))
  end
end

Use allow for stubs that may or may not be called:

it 'logs when payment fails' do
  allow(payment_gateway).to receive(:charge).and_raise(PaymentError)
  expect(logger).to receive(:error).with('Payment failed')
  processor.process(order)
end

Method Stubs

Stub methods directly on objects:

it 'sends an email notification' do
  user = User.new(email: 'test@example.com')
  allow(user).to receive(:send_welcome_email)
  
  user.register
  
  expect(user).to have_received(:send_welcome_email)
end

Shared Examples

Shared examples let you reuse test logic across multiple contexts:

RSpec.shared_examples 'a CRUD resource' do
  describe '#create' do
    it 'creates a new resource' do
      expect(subject.create(valid_attributes)).to be_persisted
    end
  end

  describe '#update' do
    it 'updates existing resource' do
      resource = subject.create(valid_attributes)
      resource.update(name: 'New Name')
      expect(resource.name).to eq('New Name')
    end
  end
end

RSpec.describe Post do
  it_behaves_like 'a CRUD resource'
end

RSpec.describe Comment do
  it_behaves_like 'a CRUD resource'
end

Test Organization Tips

Group tests logically by method, feature, or behavior. Use context to describe different scenarios:

RSpec.describe User do
  describe '#authenticate' do
    context 'with valid credentials' do
      it 'returns the user' do
        user = User.create(email: 'test@example.com', password: 'secret')
        expect(User.authenticate('test@example.com', 'secret')).to eq(user)
      end
    end

    context 'with invalid password' do
      it 'returns false' do
        user = User.create(email: 'test@example.com', password: 'secret')
        expect(User.authenticate('test@example.com', 'wrong')).to be_falsey
      end
    end
  end
end

When to Use RSpec

RSpec excels at testing complex business logic, domain models, and service objects. Its readable syntax makes tests serve as documentation. Use RSpec when:

  • You’re building domain logic that needs clear specifications
  • Your team values readable, self-documenting tests
  • You need powerful mocking and stubbing capabilities

For simple scripts or quick validation, Ruby’s built-in MiniTest may be sufficient.

Summary

You now have the tools to write comprehensive RSpec tests. Remember to keep tests focused, use descriptive names, and leverage hooks and shared examples to reduce duplication. Good tests give you confidence to refactor and ship faster.