100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Ruby

RSpec Testing Basics

Get started with RSpec, Ruby's most popular behavior-driven testing framework, covering describe/it blocks, expectations, matchers, and test organization with let and before hooks.

Gems, Testing & Rails BasicsIntermediate9 min readJul 9, 2026
Analogies

RSpec Testing Basics

RSpec is Ruby's dominant behavior-driven development (BDD) testing framework, used to describe expected behavior in a readable, English-like syntax that still executes as real, precise test code. Rather than writing assertEqual(a, b) as in xUnit-style frameworks, RSpec expresses intent with expect(a).to eq(b) — a small but meaningful shift toward specifications that read like documentation of what the code should do. It's distributed as a set of gems (rspec-core, rspec-expectations, rspec-mocks) and is typically added to a project via Bundler and configured with a spec_helper.rb or rails_helper.rb.

🏏

Cricket analogy: RSpec's expect(a).to eq(b) reads like a commentator narrating expected outcomes rather than a scorer's raw tally sheet — similar to describing 'the batter should reach fifty' as readable prose instead of just logging a number, gems bundled via Bundler like kit in a cricket bag.

describe, context, and it blocks

describe groups examples around a class, method, or feature; context is a semantic alias for describe conventionally used to group examples under a particular condition or state ("when the account is empty", "with invalid input"). Each individual test lives inside an it block, whose string argument should read naturally after "it" — together they form a sentence describing behavior.

🏏

Cricket analogy: describe groups examples around a specific batter or bowler, context narrows to a game state like 'when chasing under pressure', and each it block reads as a sentence, like 'it hits a boundary off the last ball'.

ruby
# spec/account_spec.rb
require "account"

RSpec.describe Account do
  describe "#withdraw" do
    context "when the balance is sufficient" do
      it "reduces the balance by the withdrawn amount" do
        account = Account.new(100)
        account.withdraw(30)
        expect(account.balance).to eq(70)
      end
    end

    context "when the balance is insufficient" do
      it "raises InsufficientFundsError" do
        account = Account.new(10)
        expect { account.withdraw(50) }.to raise_error(InsufficientFundsError)
      end
    end
  end
end

Notice expect { ... }.to raise_error(...) uses a block, while expect(account.balance).to eq(70) uses a value directly — RSpec requires the block form specifically for matchers that need to observe an action happening (raising an error, changing a value) rather than inspecting a value that already exists.

let, let!, and before hooks

Repeating setup code (like account = Account.new(100)) across many examples is tedious and error-prone. let(:name) { ... } defines a memoized helper method: the block runs lazily, only the first time name is referenced in a given example, and its value is cached for the rest of that example. before(:each) (the default scope) runs a block before every example in its scope, useful for side-effecting setup rather than defining a value. let! forces the let block to run eagerly before each example, useful when the setup has necessary side effects even if the example itself never directly references the let-defined name.

🏏

Cricket analogy: Instead of re-padding up before every net session, let(:account) is like a coach setting up practice gear lazily only when a player actually steps up to bat, memoized for that session, while let! sets it up eagerly before every drill regardless.

ruby
RSpec.describe Account do
  let(:account) { Account.new(100) }

  before do
    allow(Logger).to receive(:info)  # stub out logging noise
  end

  it "starts with the given balance" do
    expect(account.balance).to eq(100)
  end

  it "allows a valid withdrawal" do
    account.withdraw(40)
    expect(account.balance).to eq(60)
  end
end

let is memoized per example, not shared across examples — each it block gets a completely fresh evaluation of the let block, so mutating the object in one test never leaks into another. Relying on shared mutable state between examples (e.g. via a global variable or class-level instance variable) instead of let is a common source of flaky, order-dependent test suites.

Common matchers

RSpec ships a large library of matchers beyond eq: be compares object identity, include checks membership in a collection or substring in a string, be_truthy/be_falsey check general truthiness rather than exact equality, change { ... }.by(n) asserts a numeric delta, and raise_error asserts an exception class (and optionally message) was raised. Custom matchers can also be defined for domain-specific assertions.

🏏

Cricket analogy: change { ... }.by(n) is like checking a batter's score rose by exactly six runs after one shot, while include checks if a player's name is in the squad list and raise_error checks that a no-ball was correctly called.

ruby
expect([1, 2, 3]).to include(2)
expect("hello world").to include("world")
expect(nil).to be_falsey
expect { account.withdraw(10) }.to change { account.balance }.by(-10)
  • RSpec expresses tests as readable specifications using describe/context/it, backed by exact, executable expectations.
  • expect(value).to matcher(...) checks a value directly; expect { block }.to matcher(...) is required for matchers observing an action (raising, changing).
  • let(:name) { ... } lazily memoizes a helper value per example; let! forces it to run eagerly before the example.
  • before(:each) runs setup code before every example in its scope, distinct from let which defines a named, lazy value.
  • let-defined values are fresh for every example, preventing state leakage between tests — unlike shared instance/global variables.
  • RSpec's matcher library (eq, include, change, raise_error, be_truthy, etc.) covers most common assertion needs out of the box.

Practice what you learned

Was this page helpful?

Topics covered

#Ruby#RubyStudyNotes#Programming#RSpecTestingBasics#RSpec#Testing#Describe#Context#StudyNotes#SkillVeris