Testing with RSpec

Ruby Intermediate/Advanced
Course 2 ยท Chapter 6 ยท Testing with RSpec

๐Ÿงช Testing with RSpec

RSpec is the de facto standard Ruby testing framework โ€” not part of the standard library, but close to universal in real Ruby and Rails projects (installing it is exactly what Chapter 7's gems and Bundler chapter covers next). Its entire syntax is built from the blocks, yield, and DSL-building techniques this course has covered since Course 1 Chapter 8 โ€” a full-circle payoff worth noticing as you read through it.

๐Ÿ“ describe and it โ€” the Basic Structure

# spec/calculator_spec.rb
describe Calculator do
  it "adds two numbers correctly" do
    calc = Calculator.new
    expect(calc.add(2, 3)).to eq(5)
  end

  it "subtracts two numbers correctly" do
    calc = Calculator.new
    expect(calc.subtract(10, 4)).to eq(6)
  end
end

Spec files live in a spec/ directory and end in _spec.rb by convention. describe groups related examples (typically around a class or a feature); each it block is one individual test case with a plain-English description. Both describe and it are just methods that take a block โ€” nothing more exotic than that, though it reads almost like a small custom language.

โœ… expect().to โ€” Expectations and Matchers

expect(5).to eq(5)                     # exact equality
expect(nil).to be_nil                 # specific matcher for nil
expect([1,2,3]).to include(2)      # collection membership
expect("ruby").to start_with("ru")   # string matcher
expect { 1 / 0 }.to raise_error(ZeroDivisionError)   # wraps in a block -- must be lazy to catch the raise

The expect(...).to matcher phrasing is RSpec's core idiom, deliberately readable as close-to-English as Ruby syntax allows. Note the last example wraps the risky code in a block rather than evaluating it directly โ€” raise_error needs to control exactly when the code runs so it can catch the exception itself.

๐Ÿ”„ before Hooks โ€” Shared Setup

describe ShoppingCart do
  before(:each) do
    @cart = ShoppingCart.new   # runs fresh before EVERY example below
  end

  it "starts empty" do
    expect(@cart.items).to be_empty
  end

  it "adds an item" do
    @cart.add("Widget")
    expect(@cart.items.count).to eq(1)
  end
end

before(:each) runs before every it block in its describe, giving each test a fresh @cart instead of one shared across examples (which would let one test's state leak into another). This plays the same role as PHPUnit's setUp() method or a pytest fixture โ€” different mechanism, same underlying purpose.

๐ŸŽญ Mocks and Stubs

Real tests often need to isolate the code under test from something slow, unreliable, or external โ€” a network call, a database, another class entirely. RSpec's doubles (fake objects) and stubs (fake method responses) handle this:

describe WeatherReporter do
  it "reports sunny weather" do
    fake_api = double("WeatherAPI")
    allow(fake_api).to receive(:current_conditions).and_return("sunny")   # a stub

    reporter = WeatherReporter.new(fake_api)
    expect(reporter.report).to eq("It's sunny today!")
  end

  it "calls the API exactly once" do
    fake_api = double("WeatherAPI")
    expect(fake_api).to receive(:current_conditions).and_return("rainy")   # a mock -- verifies the call happens

    WeatherReporter.new(fake_api).report
  end
end

allow().to receive() โ€” a stub

Configures what a method returns if it's called, without requiring that it be called at all. Use this to control a fake dependency's behavior so the code under test has something predictable to work with.

expect().to receive() โ€” a mock

Asserts the method must be called during the test, and fails the test if it never is. Use this specifically when the interaction itself โ€” "did we actually call the API?" โ€” is what you're testing, not just the resulting value.

PHPUnit reaches for the separate Mockery library (or its own built-in mock objects) for this; pytest uses unittest.mock's Mock/patch. RSpec bakes the equivalent capability directly into the core framework rather than requiring a separate library.

๐Ÿ“œ Testing Frameworks, Side by Side

PHPUnit vs pytest vs RSpec

ConceptPHPUnitpytestRSpec
Group testsA test class extending TestCaseA file/class of test_ functionsdescribe block
One test caseA method starting with testA function starting with test_it block
Assertion style$this->assertEquals(5, $result)assert result == 5expect(result).to eq(5)
Shared setupsetUp() method@pytest.fixturebefore(:each) block
MockingMockery (separate library)unittest.mock built indouble / allow / expect built in

๐Ÿ’ป Coding Challenges

Challenge 1: Your First Spec File

Given a simple StringFormatter class with a method shout(text) that returns the text uppercased with an exclamation mark appended, write a describe/it spec with two examples: one confirming correct output for a normal string, one confirming it works on an already-uppercase string.

Goal: Write your first working RSpec examples using expect().to eq.

โ†’ Solution

Challenge 2: before(:each) for Shared Setup

Write specs for a Stack class (with push, pop, and empty? methods) using before(:each) to create a fresh @stack before every example. Write at least three it blocks covering pushing, popping, and the empty check.

Goal: Practice avoiding shared, leaking state between test examples.

โ†’ Solution

Challenge 3: Stubbing a Dependency

Given a class NotificationService that takes a mailer object in its constructor and calls mailer.send_email(message) inside a notify(message) method, write a spec using double and allow().to receive() to stub the mailer, then a second example using expect().to receive() to verify notify actually calls send_email.

Goal: Practice the difference between a stub (configuring a return value) and a mock (verifying a call happened).

โ†’ Solution

๐Ÿ’Ž RSpec Is Blocks All the Way Down

Every piece of RSpec's syntax โ€” describe, it, before, even the expect { ... }.to raise_error form โ€” is an ordinary Ruby method that happens to accept a block. There's no special testing syntax baked into the language; RSpec is a library built entirely from the same block mechanics Course 1 Chapter 8 called "the single feature most responsible for Ruby's reputation as an unusually expressive language." If RSpec's DSL feels like a small custom language, that's because it genuinely is one โ€” built entirely out of tools you already have.

๐ŸŽฏ What's Next

Next chapter: Gems & Bundler โ€” how RSpec (and every other library used so far) actually gets installed, what a Gemfile does, and building a small gem of your own.