skip to content

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%

answer

  1. order matters to eq
  2. same elements, any order
  3. duplicates still count
  4. match_array takes one array
  5. missing and extra elements listed

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.

solid answer

~40 s

`eq(["A1", "B2"])` passes only when the array equals that array, order included, so it fails when the database returns the rows in another order. `contain_exactly("A1", "B2")` checks the same elements and counts in any order: an extra `"A1"` fails it, and so does a missing `"B2"`. `match_array(["A1", "B2"])` is the same matcher taking one array argument, handy when the expected list is already an array. `include("A1")` passes as long as the listed elements are present, so it is the choice when extra items are allowed. The failure message of `contain_exactly` lists the missing elements and the extra elements separately, which tells the reader at a glance whether something was dropped or duplicated. Elements can be matchers too: `contain_exactly(a_string_starting_with("A"), "B2")`.

go deeper

for a junior

Know that eq cares about order while contain_exactly and match_array do not, and that include allows extras.

for a middle

Explain the multiset behaviour of contain_exactly, the one-array shape of match_array, and how its failure message lists missing and extra elements.

for a senior

Replace order-sensitive eq checks on unsorted query results that fail intermittently, and compose attribute matchers for object collections.

for a principal

Push for matchers that state the real contract of a collection, so ordering guarantees are explicit in the code, not implied by specs.

## Four questions about a collection Given `order.skus` returning an Array of line-item codes, a spec can ask four different questions. RSpec has a matcher for each: | Question | Matcher | Order matters | Extras allowed | Duplicates count | |---|---|---|---|---| | exactly this list, in this order? | `eq(["A1", "B2"])` | yes | no | yes | | exactly these elements, any order? | `contain_exactly("A1", "B2")` | no | no | yes | | same, expected given as one array? | `match_array(["A1", "B2"])` | no | no | yes | | at least these elements? | `include("A1", "B2")` | no | yes | no | ## contain_exactly and match_array `match_array(items)` is literally `contain_exactly(*items)`: one matcher, two call shapes. Choose by what you have at hand: ```ruby expect(order.skus).to contain_exactly("A1", "B2") expect(order.skus).to match_array(expected_skus) ``` Calling `match_array("A1", "B2")` with two arguments raises `ArgumentError`, because the method takes exactly one. The matcher treats both sides as **multisets**: each expected element must pair with a distinct actual element. So: - `["A1", "A1", "B2"]` fails `contain_exactly("A1", "B2")`; the second `"A1"` is an extra. - `["B2", "A1"]` passes; order is ignored. - `["A1"]` fails; `"B2"` is missing. The actual value can be anything that converts to an Array through `to_ary` or `to_a`, such as a Set or a query result; otherwise the failure message says it expected a collection. ## Why not eq everywhere `eq` is correct when order is part of the contract, such as SKUs sorted for a receipt. When order is incidental, for example rows loaded without an explicit sort, `eq` makes the spec fail whenever storage order changes, a classic source of intermittent failures. Sorting both sides (`expect(order.skus.sort).to eq(...)`) works but hides intent and loses the failure detail below. ## Why not include `include` accepts extras, so a bug that adds a duplicate or an unexpected free item passes. Use it when extras are legitimately allowed, such as checking that a promotional item is present among whatever else the customer chose. For hashes, `include(sku: "A1")` checks a subset of keys and values. ## Reading the failure When `contain_exactly` fails, it prints three lists: 1. the expected collection; 2. the actual collection; 3. the missing elements and the extra elements, each on its own line. That split turns a wall of values into a diagnosis: "missing B2, extra A1" reads immediately as a duplicated line item that replaced another. ## Related collection matchers - `all(matcher)` checks every element: `expect(order.line_items).to all(have_attributes(qty: be > 0))`. - `start_with` and `end_with` check an ordered prefix or suffix of an Array, useful when only the first items are guaranteed. - `include` on a Hash checks a subset of keys, or of key-value pairs. - `be_empty` checks that nothing is there at all, reading better than `eq([])`. Picking among them is picking the contract: whole list in order, whole list in any order, some members, or a property of every member. Each one fails with a message shaped to that question. ## Composing with other matchers Expected elements are compared with RSpec's matching rules, so they may be matchers themselves: ```ruby expect(order.line_items).to contain_exactly( an_object_having_attributes(sku: "A1", qty: 3), an_object_having_attributes(sku: "B2", qty: 1) ) ``` - Aliases such as `a_string_starting_with`, `an_object_having_attributes` and `a_value_within` exist so nested expectations read as noun phrases. - The matcher searches for the best pairing, so overlapping matchers still pair correctly. - The negation, `not_to contain_exactly`, is rarely useful; state what the collection should contain instead.

  • Why does `expect(order.skus).to contain_exactly("A1", "B2")` fail for ["A1", "A1", "B2"]?
    The matcher pairs each expected element with a distinct actual element. Two expected elements cannot cover three actual ones, so the second "A1" is reported as an extra element. Duplicates count, unlike with `include`.
  • How would you check line items by attributes without caring about their order?
    Pass matchers as elements: `contain_exactly(an_object_having_attributes(sku: "A1"), an_object_having_attributes(sku: "B2"))`. Each element is matched with RSpec's matching rules, so the line items only need those attributes, in any order.

saying these in an interview costs you the question

  • contain_exactly ignores duplicate elements
  • match_array accepts the expected elements as separate arguments
  • include fails when the collection has extra elements
  • eq compares arrays regardless of element order