skip to content

Expectations & Matchers

The expect(...).to syntax, the built-in matchers from eq and be to change, raise_error and output, and custom matchers. Interviewers check you pick the matcher whose failure message explains the bug.

on this pageshow

explore

questions

6

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%

answer

  1. each matcher calls one Ruby method
  2. eq uses ==
  3. eql uses eql?, type-strict for numbers
  4. equal and be(x) use equal?
  5. bare be means truthy

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.

solid answer

~40 s

Each matcher delegates to one Ruby comparison. `eq(1999)` calls `==`, so `expect(order.total_cents).to eq(1999.0)` passes because `1999 == 1999.0`. `eql(1999.0)` calls `eql?`, which also compares numeric type, so the same Integer fails it. `equal(x)`, and `be(x)` with one argument, call `equal?` and pass only for the identical object: `expect(line_item.order).to be(order)` proves the association returns that exact instance, while two equal but separate arrays fail. With no argument, `be` passes for anything except `nil` and `false`, so `expect(order.discount).to be` passes for `0`. The failure messages say which method was used, such as `(compared using ==)`, which tells the reader what kind of equality failed. Reach for `eq` by default and the others only when type or identity is the point.

go deeper

for a junior

Recall which Ruby method each matcher calls: == for eq, eql? for eql, equal? for equal and be(x), truthiness for bare be.

for a middle

Explain why eq(1999.0) passes and eql(1999.0) fails for an Integer, and when identity with be(obj) is the real contract.

for a senior

Pick the matcher whose failure message names the right kind of mismatch, and catch specs that pass by accident through bare be.

for a principal

Set a team default of eq for values with explicit exceptions, so reviewers can read intent from the matcher alone.

## One matcher, one Ruby method RSpec's equality matchers do not define equality themselves. Each one calls a Ruby method on the **actual** value (the thing inside `expect(...)`) and passes if it returns truthy: | Matcher | Calls | Passes when | Typical use | |---|---|---|---| | `eq(expected)` | `actual == expected` | values are equal | almost every value check | | `eql(expected)` | `actual.eql?(expected)` | equal and, for numbers, same class | type-strict value checks | | `equal(expected)` | `actual.equal?(expected)` | same object | identity of an instance | | `be(expected)` | `actual.equal?(expected)` | same object | identity, reads well with `nil`, `true` | | `be` (no argument) | truthiness | actual is neither `nil` nor `false` | presence of any value | What `==`, `eql?` and `equal?` mean for a given class is Ruby's business; RSpec only chooses which one to call. That is why a custom class that defines `==` works with `eq` automatically. ## Checking an order total Suppose `order.total_cents` returns the Integer `1999`: ```ruby expect(order.total_cents).to eq(1999) # passes expect(order.total_cents).to eq(1999.0) # passes: 1999 == 1999.0 expect(order.total_cents).to eql(1999.0) # fails: Integer vs Float expect(order.total_cents).to eql(1999) # passes ``` `eql` is the matcher to use when the **type** is part of the contract, for example when a rounding change must not silently turn an Integer total into a Float. For most totals `eq` is right, and a Float total compared with `eq` against a decimal literal may fail on rounding; `be_within(0.01).of(19.99)` exists for that case. ## Checking object identity `equal` and single-argument `be` are the same check. They answer "is this the very object I already hold?": ```ruby expect(line_item.order).to be(order) # same instance expect(order.line_items).to equal(order.line_items) # true only if memoized expect([1, 2]).to equal([1, 2]) # fails: two arrays ``` Identity matters when code promises to return a cached or shared instance. For plain values it is usually the wrong question: two separately built arrays with the same contents are `==` but not `equal?`. Small Integers, Symbols, `nil`, `true` and `false` are single objects in Ruby, so `be(nil)` and `be(true)` behave as expected; `be_nil`, `be_truthy` and `be_falsey` read better for those. ## Bare be and truthiness With no argument, `be` becomes a truthiness matcher: - `expect(order.discount).to be` passes for `0`, `""` and `[]`, because Ruby treats every value except `nil` and `false` as truthy. - `be_truthy` and `be_falsey` state the same intent explicitly. - `be true` is different: it calls `equal?(true)` and fails for `1` or `"yes"`. Operators hang off bare `be` as well: `expect(order.total_cents).to be > 0` compares with `>`. ## Reading the failure Each equality failure prints the expected and actual values and names the comparison, for example `(compared using ==)` or `(compared using eql?)`. `eq` and `eql` also print a diff for multi-line strings and larger structures. Choosing the matcher that matches the intent pays off here: 1. A total that is off by one shows two numbers and `==`, which points at arithmetic. 2. An `eql` failure between `1999` and `1999.0` points at a type change. 3. An identity failure between two identical-looking objects points at a missing cache or a copy. ## Value objects `eq` is only as good as the class's `==`. Two practical consequences: - A `Data.define` value object, such as `Money = Data.define(:cents, :currency)`, defines `==` and `eql?` by its members, so `eq(Money.new(cents: 1999, currency: "EUR"))` works out of the box. - A plain class that never defined `==` inherits identity from `Object`, so `eq` on two separately built instances fails exactly like `equal`. The fix is to define equality on the class, or to compare attributes with `have_attributes`. ## Common mistakes - Using `equal` for strings or arrays and being surprised when equal contents fail. - Writing `expect(x).to be` intending "is true" and letting `0` or an empty string pass. - Expecting `eq` to be type-strict; it is only as strict as the class's `==`.

  • Why does `expect([1, 2]).to eq([1, 2])` pass while `expect([1, 2]).to equal([1, 2])` fails?
    `eq` calls `==`, and arrays compare element by element. `equal` calls `equal?`, which is identity, and the two literals build two separate Array objects, so the identity check fails even though the contents match.
  • How would you assert a Float order total such as 19.99 without a rounding failure?
    Use `be_within(0.01).of(19.99)`, which passes when the actual value lies within the delta of the expected one. Better still, store money as Integer cents so `eq(1999)` is exact.

saying these in an interview costs you the question

  • eq checks that both sides are the same object
  • eql and eq are interchangeable for numbers
  • be(order) compares the order's attributes
  • expect(x).to be only passes when x is true
  • equal is the right matcher for comparing two strings
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 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, 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