In RSpec, how do the eq, eql, equal and be matchers differ when a spec checks an order total or an order object?
answer
- each matcher calls one Ruby method
- eq uses ==
- eql uses eql?, type-strict for numbers
- equal and be(x) use equal?
- bare be means truthy
basics
~20 seq 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 sEach 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
Recall which Ruby method each matcher calls: == for eq, eql? for eql, equal? for equal and be(x), truthiness for bare be.
Explain why eq(1999.0) passes and eql(1999.0) fails for an Integer, and when identity with be(obj) is the real contract.
Pick the matcher whose failure message names the right kind of mismatch, and catch specs that pass by accident through bare be.
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