Testing with RSpec
🧪 Testing with RSpec
📁 describe and it — the Basic Structure
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
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
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:
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
| Concept | PHPUnit | pytest | RSpec |
|---|---|---|---|
| Group tests | A test class extending TestCase | A file/class of test_ functions | describe block |
| One test case | A method starting with test | A function starting with test_ | it block |
| Assertion style | $this->assertEquals(5, $result) | assert result == 5 | expect(result).to eq(5) |
| Shared setup | setUp() method | @pytest.fixture | before(:each) block |
| Mocking | Mockery (separate library) | unittest.mock built in | double / 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.
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.
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).
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.