Skip to content

What it is

RSpec is Ruby's behaviour-driven testing framework, with nested example groups, expressive matchers and built-in test doubles.

describe and context nest example groups; it declares an example. let defines lazily-evaluated helpers, and subject names the object under test.

Installation

gem 'rspec-rails', group: %i[development test]

Getting started

The smallest useful thing you can do with it, and what each part means.

Structure, let and matchers
RSpec.describe BookService do
  # Lazily evaluated, memoised per example.
  let(:repository) { instance_double(BookRepository) }
  subject(:service) { described_class.new(repository) }

  describe '#title_for' do
    context 'when the book exists' do
      before do
        allow(repository).to receive(:find).with(42)
          .and_return(Book.new(title: 'Dune'))
      end

      it 'returns the title' do
        expect(service.title_for(42)).to eq('Dune')
      end
    end

    context 'when the book is missing' do
      before { allow(repository).to receive(:find).and_return(nil) }

      it 'raises NotFound' do
        expect { service.title_for(99) }.to raise_error(BookService::NotFound)
      end
    end
  end
end
instance_double verifies that the stubbed method actually exists on the real class, so a renamed method breaks the test instead of silently passing against a fictional API.

Advanced usage

Where the library earns its place over a simpler alternative.

Shared examples and custom matchers
RSpec.shared_examples 'a soft-deletable record' do
  it 'sets deleted_at instead of destroying' do
    subject.soft_delete!
    expect(subject.deleted_at).to be_present
    expect(described_class.unscoped).to include(subject)
  end
end

RSpec.describe Book do
  it_behaves_like 'a soft-deletable record'
end

RSpec::Matchers.define :be_published_in do |year|
  match { |book| book.year == year }
  failure_message { |book| "expected #{year}, got #{book.year}" }
end

expect(book).to be_published_in(1965)

# Change matchers express intent better than before/after comparisons.
expect { service.create(params) }.to change(Book, :count).by(1)
Shared examples let one specification cover every class with a given behaviour, which keeps cross-cutting contracts tested without copying the same block into ten files.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

does not implement: find
instance_double is verifying — the method does not exist on the real class. Fix the name, or the class no longer has that API.
Specs pass alone but fail together
Leaked global state. Enable config.order = :random to surface order dependence early.

Best practices

  • Use instance_double and class_double rather than plain double so stubs are verified against the real API.
  • Keep let for setup that most examples need; too many lets make a spec hard to follow.
  • One expectation per example where practical — failures then name the exact behaviour.
  • Use the change matcher rather than manual before-and-after counting.

Background

Why it exists, and what it was reacting to.

RSpec popularised behaviour-driven development in Ruby. Its readable describe/context/it structure influenced testing frameworks across many other languages.