skip to content

RSpec

Ruby's BDD framework: nested describe and context groups, the expect matcher DSL, verifying doubles, and random-order runs. Its style choices shape how a whole Ruby codebase is tested.

on this pageshow

explore

questions

23

In rspec-mocks, what is the difference between allow(obj).to receive(:msg) and expect(obj).to receive(:msg)?

level: juniorimportance: must knowfreq 72%

answer

  1. one permits, one demands
  2. allowed message returns nil
  3. checked when the example ends
  4. must be set before the call
  5. exactly once unless told otherwise

basics

~20 s

allow(obj).to receive(:msg) permits a message and configures its reply; the example passes whether or not it is sent. expect(obj).to receive(:msg) also demands it: unless it arrives exactly once by the end of the example, the example fails.

solid answer

~40 s

Both take the same `receive(:msg)` object with the same fluent interface (`with`, `and_return`, `and_raise`, count constraints), and both work on a plain `double` or on a real object. `allow` only **permits** the message: with no response configured it returns `nil`, and nothing checks whether it was ever sent. `expect(...).to receive` **also sets a message expectation** that rspec-mocks verifies when the example finishes: by default the message must arrive **exactly once**, otherwise the failure reads `expected: 1 time ... received: 0 times`. Because the expectation is registered up front, it has to be written **before** the code under test runs. Use `allow` for a collaborator whose answer you need, `expect` only for the call that is the behaviour you are testing, and `expect(...).not_to receive` when a message must never be sent.

code

ruby · 14 lines
ruby
RSpec.describe NotificationService do
  let(:gateway) { double("sms gateway") }
  let(:service) { NotificationService.new(gateway:) }

  it "texts the user" do
    expect(gateway).to receive(:deliver).with(to: "+15550100", body: "Hi")
    service.notify(phone: "+15550100", text: "Hi")
  end

  it "records the receipt the gateway returns" do
    allow(gateway).to receive(:deliver).and_return("msg-42")
    expect(service.notify(phone: "+15550100", text: "Hi")).to eq("msg-42")
  end
end

go deeper

for a junior

Remember the one-line rule: allow permits and supplies an answer, expect also demands the call. Know that an un-configured stub returns nil and that expect must be set before the action.

for a middle

Explain that expect registers a message expectation verified when the example ends, that the default count is exactly once, and how with, once, twice and at_least change what is checked.

for a senior

Show judgement about how many expects an example carries: one demanded call per behaviour, allow for everything incidental, so refactors that change call counts do not break unrelated examples.

for a principal

Frame the choice as a suite-wide convention: reviewers should question examples whose message expectations restate the implementation, because that coupling is what makes a large RSpec suite expensive to change.

## Two verbs, one message builder rspec-mocks, the mocking library that ships with RSpec 3.13, lets a spec configure how an object reacts to a **message** (a method call). The configuration always has the same shape: ```ruby allow(gateway).to receive(:deliver).and_return(true) expect(gateway).to receive(:deliver).with(to: "+15550100", body: "Hi") ``` `receive(:deliver)` builds the description of the message; the verb in front of it decides what RSpec does with that description. The object can be a **pure test double** made with `double` or `instance_double`, or a **partial double**, meaning a real object (or class) with just this one method replaced for the duration of the example. ## What allow does `allow(obj).to receive(:msg)` **permits** the message: - a strict `double` that would otherwise raise `received unexpected message :msg` now accepts it; - the reply is whatever you configure (`and_return`, `and_raise`, a block), and **`nil`** if you configure nothing; - nothing is checked at the end of the example; zero calls or fifty calls both pass. That makes `allow` the tool for a collaborator whose *answer* the code under test needs, such as a gateway that must return a delivery receipt so the service reaches its next branch. ## What expect ... to receive adds `expect(obj).to receive(:msg)` does everything `allow` does and also registers a **message expectation**. When the example completes, rspec-mocks verifies every registered expectation: 1. by default the message must have been received **exactly once**; zero calls and two calls both fail; 2. `with(...)` constrains the arguments; a call with other arguments fails, reported as `with unexpected arguments`, unless an `allow` stub for the same message catches it; 3. count constraints change the rule: `once`, `twice`, `thrice`, `exactly(3).times`, `at_least(:once)`, `at_most(2).times`. A failed expectation is reported like this: ``` (Double "sms gateway").deliver(*(any args)) expected: 1 time with any arguments received: 0 times with any arguments ``` `expect(obj).not_to receive(:msg)` is the negative form: any receipt of the message fails the example immediately. ## Order matters: expectation first, action second Because `expect ... to receive` installs its expectation when that line runs, it must come **before** the code under test. Written after the call, it installs a fresh expectation that nothing will satisfy, and the example fails with `received: 0 times` even though the call really happened. If you prefer to assert after acting, the spy style (`allow` or `spy`, then `expect(obj).to have_received(:msg)`) exists for exactly that. ## Side by side | | `allow(obj).to receive(:msg)` | `expect(obj).to receive(:msg)` | |---|---|---| | Unexpected-message error on a double | suppressed | suppressed | | Default return value | `nil` | `nil` | | Verified at end of example | no | yes, exactly once by default | | Placement | anywhere before the call | before the call | | Typical use | supply a collaborator's answer | assert the call that *is* the behaviour | ## How a failure surfaces When a strict double receives a message nobody allowed, or a message expectation is not met, rspec-mocks raises `RSpec::Mocks::MockExpectationError`. That class inherits from **`Exception`**, not `StandardError`, which matters for code under test that protects itself with a bare `rescue => e`: - a bare `rescue` only catches `StandardError` and its subclasses; - so a service that wraps its gateway call in `rescue => e` and logs the error does **not** swallow the mock failure; - the example still fails with the rspec-mocks message instead of passing silently. Unmet `expect ... to receive` expectations are checked after the example body has run, so the failure is reported against the line that set the expectation, which is why reading the `Failure/Error:` line points you at the `expect`, not at the production call. ## Choosing between them A useful rule is that each example should demand only what it is about. In a notification-service spec, the example "sends the text through the SMS gateway" earns one `expect(gateway).to receive(:deliver)`; every other example that merely needs the gateway to cooperate uses `allow`. Turning every `allow` into `expect` welds the spec to the implementation: harmless refactors that change how often a collaborator is asked start failing examples that were never about that call. ## Common mistakes - Writing `expect(...).to receive` after the action and expecting it to check the past. - Believing `allow` fails when the message is never sent; it never checks. - Forgetting that an un-configured stub returns `nil`, then chasing a `NoMethodError` on `nil` further down. - Assuming a plain `expect(...).to receive` accepts any number of calls; the default is exactly one.

  • What happens if the code under test calls a message twice that was set up with a plain expect(obj).to receive(:msg)?
    The example fails. A message expectation with no count constraint expects exactly one call, so the second call leaves the actual count at 2 and verification at the end of the example reports `expected: 1 time ... received: 2 times`. Add `twice`, `exactly(n).times` or `at_least(:once)` when more than one call is legitimate.
  • Can allow and expect be combined on the same message in one example?
    Yes. A common pattern is a broad `allow(gateway).to receive(:deliver).and_return(true)` in a `before` hook, then `expect(gateway).to receive(:deliver).with(to: "+15550100", body: anything)` inside the one example that is about that call. The expectation is checked, and calls it does not match fall back to the allowed stub.

saying these in an interview costs you the question

  • allow(...).to receive fails the example when the message is never sent
  • expect(...).to receive can be written after the action and still check it
  • an allowed message with no and_return returns the real method's result
  • a plain expect(...).to receive accepts any number of calls
  • every collaborator call should be an expect so nothing slips through
open as a page

In an RSpec spec, how do describe, context and it relate to each other, and what does nesting groups actually build?

level: juniorimportance: must knowfreq 62%

basics

~20 s

describe and context both create an example group and behave identically; it defines one example in a group. A nested group is a subclass of its parent, inheriting let, hooks and helpers, and each example runs in a fresh group instance.

open as a page

In RSpec, how do the eq, eql, equal and be matchers differ when a spec checks an order total or an order object?

level: juniorimportance: must knowfreq 76%

basics

~20 s

eq passes when actual == expected, eql when actual.eql?(expected), and equal or be(x) only when both are the same object (equal?). Bare be with no argument passes for any truthy value. eq is the default for values.

open as a page

In an RSpec spec, how does instance_double differ from a plain double, and what does it check against the real class?

level: middleimportance: must knowfreq 62%

basics

~20 s

A plain double accepts stubs for any method name, so it stays green when the real class changes. instance_double(SmsGateway) only lets you stub or expect instance methods SmsGateway really defines, and checks calls against their parameters.

open as a page

In RSpec, how do let, let! and an instance variable assigned in a before hook differ in when setup runs?

level: middleimportance: must knowfreq 74%

basics

~20 s

let is lazy, running its block on first call and memoizing the value for one example. let! also adds a before hook, so it always runs. An instance variable set in before always runs too, and reads as nil if misspelled.

open as a page

In RSpec, how do you assert that order.checkout! raises OutOfStockError with a specific message, and what makes such a spec pass by accident?

level: middleimportance: must knowfreq 64%

basics

~20 s

Wrap the call in a block: expect { order.checkout! }.to raise_error(OutOfStockError, /SKU A1/). Pass both a class and a message; a bare raise_error also matches a NoMethodError from a typo, so the spec can pass without reaching the code.

open as a page

In RSpec, how do you run examples in random order, and how do you replay the exact order of a failed run with --seed?

level: middleimportance: must knowfreq 58%

basics

~20 s

RSpec runs examples in defined order unless config.order = :random or --order rand is set. A random run ends by printing its seed, such as Randomized with seed 4821, and rspec --seed 4821 replays exactly that order.

open as a page

In RSpec 3.13, what does rspec --init create, and which settings in the generated spec_helper.rb are actually switched on?

level: juniorimportance: should knowfreq 45%

basics

~10 s

rspec --init writes .rspec, holding --require spec_helper, and spec/spec_helper.rb. Only three settings in that file are active; random order, :focus filtering, failure persistence and profiling sit in a commented-out =begin block.

open as a page

In rspec-mocks, what do and_return, and_raise and and_call_original make a stubbed message do, and where is and_call_original unavailable?

level: middleimportance: should knowfreq 45%

basics

~20 s

and_return sets the reply, and with several values returns them in order, repeating the last. and_raise raises an exception class or instance. and_call_original runs the real method, so it works only on partial doubles, never on a pure double.

open as a page

In an RSpec spec, how does asserting with spy and have_received differ from expect(...).to receive, and when does have_received refuse to work?

level: middleimportance: should knowfreq 48%

basics

~10 s

expect(...).to receive is set before the action; have_received checks afterwards that a recorded message arrived. It needs a spy or a message allowed beforehand, otherwise it fails: not a spy or method not stubbed.

open as a page

In RSpec, what is the implicit subject of RSpec.describe ShoppingList, and how do named subjects and is_expected change a spec?

level: middleimportance: should knowfreq 52%

basics

~20 s

When the outer group describes a class, subject is ShoppingList.new, built lazily and memoized per example like a let. subject(:list) { ... } replaces it and also names it; is_expected is shorthand for expect(subject) in one-line examples.

open as a page

In RSpec, how do the block matchers change, output and yield_control observe what the code in expect { } did?

level: middleimportance: should knowfreq 46%

basics

~20 s

change { order.total_cents } evaluates the value before and after running the expect block and compares them, refined by by, from and to. output(...).to_stdout captures what the block prints; yield_control checks that the method yielded to the probe block it was given.

open as a page

In RSpec, when should a spec for an order's line-item SKUs use contain_exactly or match_array instead of eq or include?

level: middleimportance: should knowfreq 42%

basics

~10 s

Use contain_exactly("A1", "B2") or match_array(["A1", "B2"]) when the collection must hold exactly those elements in any order. eq also demands the order; include only demands the listed elements and ignores extras.

open as a page

In RSpec, what do --only-failures, --next-failure and --profile each do, and what must spec_helper.rb set before --only-failures works?

level: middleimportance: should knowfreq 32%

basics

~10 s

--only-failures reruns just the examples that failed last time and needs config.example_status_persistence_file_path set. --next-failure also stops at the first failure in defined order. --profile reports the slowest examples and groups, ten by default.

open as a page

In RSpec, how do --tag filters and config.filter_run_when_matching :focus decide which examples run, and why can a committed fit shrink a CI run?

level: middleimportance: should knowfreq 42%

basics

~20 s

--tag slow runs only examples whose metadata has slow set, and --tag ~slow excludes them. filter_run_when_matching :focus narrows a run to focused examples only when some exist, so a fit left in a commit makes CI run just that one.

open as a page

An RSpec 3.13 spec stubs SmsGateway.deliver on the real class and still passes after deliver was renamed; what does verify_partial_doubles change, and what does it still miss?

level: seniorimportance: should knowfreq 38%

basics

~20 s

With verify_partial_doubles off, which is rspec-mocks' default in RSpec 3.13, a real object can be stubbed with methods it no longer has. Turned on, stubs are checked for existence and argument signature; return values and pure doubles stay unchecked.

open as a page

In RSpec, what changes when ShoppingList spec setup moves from before(:example) to before(:context), and what stops working there?

level: seniorimportance: should knowfreq 46%

basics

~10 s

before(:context) runs once per group, not per example. Its instance variables reach every example as references to the same objects, so mutations leak between examples, and let, subject and test doubles are unsupported there.

open as a page

In RSpec, when should a spec use it_behaves_like rather than include_examples, and how does shared_context differ from shared_examples?

level: seniorimportance: should knowfreq 38%

basics

~20 s

it_behaves_like evaluates shared examples in a new nested group, isolating their lets; include_examples evaluates them in the current group, where a second inclusion overrides the first one's lets. shared_context is an alias used for setup via include_context.

open as a page

In RSpec, how does aggregate_failures change what a failing example reports, and when would you chain matchers with and or or instead?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Normally the first failed expectation raises and ends the example. Inside aggregate_failures, RSpec records every failed expectation and reports them together at the end. Compound and/or instead combine two matchers against one actual value in a single expectation.

open as a page

In RSpec, how do you write a custom matcher with RSpec::Matchers.define so an order-total failure explains itself?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Call RSpec::Matchers.define :have_total_cents do |expected| ... end with a match block returning truthy or falsy, and add failure_message showing the data needed to diagnose it. RSpec supplies a default description and message from the name and arguments.

open as a page

An RSpec suite fails in CI only with --seed 4821; how does rspec --bisect narrow the failure down, and what makes it report nothing useful?

level: seniorimportance: should knowfreq 40%

basics

~20 s

rspec --seed 4821 --bisect reruns subsets in that order: it checks the failure needs other examples first, then repeatedly discards half of the passing ones, and prints a minimal reproduction command naming the victim and the example that pollutes it.

open as a page

In an RSpec spec, what does stub_const do, and why can stub_const("SmsGateway", fake) miss the constant the code actually uses?

level: middleimportance: nice to knowfreq 26%

basics

~10 s

stub_const replaces or defines a constant for one example and restores it afterwards. Its name must be fully qualified: inside module Notifications, stub_const("SmsGateway", fake) stubs ::SmsGateway, not Notifications::SmsGateway.

open as a page

In a Rakefile, what does RSpec::Core::RakeTask.new(:spec) define, and what happens to the rake run when examples fail?

level: middleimportance: nice to knowfreq 22%

basics

~10 s

RSpec::Core::RakeTask.new(:spec) defines a rake task that runs the rspec executable in a subprocess over spec/{,/*/}/*_spec.rb. If examples fail, fail_on_error (true by default) makes rake exit with rspec's exit status.

open as a page