In RSpec, how does aggregate_failures change what a failing example reports, and when would you chain matchers with and or or instead?
answer
- first failure normally stops the example
- collect, then report together
- :aggregate_failures metadata wraps the example
- and/or combine matchers on one actual
- & and | are the aliases
basics
~20 sNormally 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.
solid answer
~40 sAn RSpec expectation failure raises, so after `expect(order.total_cents).to eq(2499)` fails, the tax and status checks below it never run and you fix bugs one run at a time. Wrapping them in `aggregate_failures "order totals" do ... end` makes RSpec collect each failure and raise one error listing all of them when the block ends; tagging the example with `:aggregate_failures` metadata applies the same to the whole example. A non-expectation exception still stops the block, but the failures recorded before it are reported with it. Compound matchers are different: `expect(order.reference).to start_with("ORD-").and end_with("-EU")` puts two conditions on **one** value in one expectation, and `or` passes if either holds. They also combine block matchers, such as two `change` matchers on one action, but they cannot be negated with `not_to`.
go deeper
Know that an example normally stops at its first failed expectation and that aggregate_failures reports them all.
Explain block and metadata forms of aggregate_failures, and how and/or combine matchers on one actual value.
Use aggregation to keep costly setup to one run while seeing every broken field, and avoid compound chains that should be have_attributes or a custom matcher.
Decide whether :aggregate_failures becomes a default for integration-style examples, weighing faster diagnosis against examples that assert too much.
## Why the first failure normally wins When an RSpec expectation fails, it raises `RSpec::Expectations::ExpectationNotMetError`. The exception unwinds the example, so every line after it is skipped. For a checkout example that verifies total, tax and status, a wrong total hides whether tax and status were also wrong, and the developer discovers the bugs one run at a time. ## aggregate_failures `aggregate_failures` changes that for a block of code: ```ruby it "prices a checked-out order" do order.checkout! aggregate_failures "order totals" do expect(order.total_cents).to eq(2499) expect(order.tax_cents).to eq(416) expect(order.status).to eq(:paid) end end ``` Inside the block, each failing expectation is **recorded** instead of raised. When the block ends, RSpec raises a single `RSpec::Expectations::MultipleExpectationsNotMetError` whose message lists every failure with its own details, under the optional label. If only one expectation failed, that failure is raised on its own. Details that matter: 1. **Whole-example form.** Tagging an example `it "prices...", :aggregate_failures do` applies the same behaviour to the entire example through a built-in `around` hook, with no explicit block. 2. **Other exceptions still stop it.** If `order.tax_cents` raises `NoMethodError`, the block stops there; RSpec reports the error together with the expectation failures already recorded, so none is lost. 3. **Setup still stops early.** Code outside the block, such as `order.checkout!`, behaves normally. ## Compound matchers `and` and `or` are methods on matchers, aliased as `&` and `|`. They build **one** matcher out of two, applied to one actual value: ```ruby expect(order.reference).to start_with("ORD-").and end_with("-EU") expect(order.status).to eq(:paid).or eq(:refunded) ``` - An `and` failure reports the sub-matcher that failed, or both if both did. - An `or` failure reports both alternatives. - Block matchers compose too: `expect { order.cancel! }.to change { order.status }.to(:cancelled).and change { order.total_cents }.to(0)` runs the block once and checks both changes. Limits enforced by rspec-expectations: - `expect(...).not_to matcher.and matcher` raises `NotImplementedError`; write positive matchers, or define a negated one with `RSpec::Matchers.define_negated_matcher`. - A block matcher and a value matcher cannot be combined in one compound expression. - Two matchers that both expect the block to jump out of the stack, such as `raise_error` and `throw_symbol`, cannot be combined. ## Choosing between them | Situation | Tool | |---|---| | two properties of one value | compound `and` | | either of two acceptable values | compound `or` | | several values produced by one action | `aggregate_failures` block | | an example whose expectations all describe one outcome | `:aggregate_failures` metadata | | several independent behaviours | separate examples | ## Common mistakes - Wrapping the **action** inside `aggregate_failures` together with the checks. If the action raises, the block stops anyway, and the real failure is buried among expectation messages. Keep the action outside. - Chaining three or four matchers with `and` on one object where `have_attributes(total_cents: 2499, tax_cents: 416, status: :paid)` would check every reader and report every mismatch in one message. - Reaching for `or` when the code should be deterministic. An `or` expectation that accepts two statuses can hide a race or a missing branch; use it only when both outcomes are legitimately correct. - Expecting `aggregate_failures` to keep going after an unrelated exception; only expectation failures are collected and continued past. ## Trade-offs - `aggregate_failures` keeps expensive setup, such as a checkout against a stored order, to one run while still showing every broken field. - It does not make an example about many behaviours a good example; the label and the grouping should describe one outcome. - Compound matchers keep a single expectation line readable, but more than two conjuncts usually read better as `have_attributes` or a custom matcher.
- What does RSpec report if exactly one expectation fails inside an aggregate_failures block?Just that failure, raised as the ordinary expectation error, not wrapped in a multiple-failures error. The aggregated error only appears when two or more failures, or a failure plus another exception, were recorded.
- Why does `expect(order.reference).not_to start_with("TMP-").and end_with("-X")` raise?Negating a compound is ambiguous: it could mean neither condition holds or not both. rspec-expectations refuses it with `NotImplementedError`. Define a negated matcher with `define_negated_matcher` and combine positives instead.
saying these in an interview costs you the question
- aggregate_failures swallows exceptions such as NoMethodError
- and runs the expect block once per matcher
- compound matchers can check several different objects in one expectation
- not_to works with compound matchers like any other matcher
- aggregate_failures is only available as example metadata