In RSpec, how do you assert that order.checkout! raises OutOfStockError with a specific message, and what makes such a spec pass by accident?
answer
- a block, not a value
- class plus string or regex
- string message must match exactly
- bare raise_error catches NoMethodError
- not_to raise_error(SomeError) warns
basics
~20 sWrap 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.
solid answer
~40 s`raise_error` needs the block form, `expect { order.checkout! }`, because the matcher must call the code itself and rescue what it raises. Writing `expect(order.checkout!)` runs `checkout!` before `expect` is even called, so the exception escapes and the example errors. The matcher accepts an exception class, which also matches subclasses, and a message: a String must equal the message exactly, while a Regexp only has to match it. A block after the matcher receives the exception for extra checks, such as `expect(e.sku).to eq("A1")`. Bare `raise_error` matches any exception, including the `NoMethodError` or `ArgumentError` Ruby raises for a typo, so RSpec warns about possible false positives by default. For the negative, write `not_to raise_error` with no arguments; `not_to raise_error(OutOfStockError)` passes when any other error is raised and triggers the same kind of warning.
go deeper
Remember the braces: expect { code }.to raise_error(SomeError), never expect(code).
Explain class-plus-message matching, exact String versus Regexp messages, and the block that receives the exception.
Hunt down bare raise_error and negative class checks that pass on typos, and tighten them to the contract callers rescue.
Decide whether a suite sets on_potential_false_positives to :raise, trading a noisy upgrade for specs that cannot pass on NoMethodError.
## Why the block form Most matchers inspect a **value**: RSpec evaluates the expression inside `expect(...)` first, then hands the result to the matcher. An exception cannot be handed over that way, because raising it aborts the expression before `expect` receives anything. `raise_error` is a **block matcher**: `expect { ... }` wraps the code in a block, and the matcher calls that block inside a `rescue`. ```ruby RSpec.describe Order do let(:order) { Order.new(items: [LineItem.new(sku: "A1", qty: 3)]) } it "refuses to check out unavailable stock" do expect { order.checkout! } .to raise_error(OutOfStockError, /SKU A1/) end end ``` Written as `expect(order.checkout!).to raise_error(OutOfStockError)`, Ruby evaluates `order.checkout!` as an argument, the error propagates out of the example, and RSpec reports the example as failed with `OutOfStockError` instead of as a passing expectation. ## What the arguments match | Call | Matches | |---|---| | `raise_error(OutOfStockError)` | that class or any subclass | | `raise_error(OutOfStockError, "out of stock: A1")` | that class, message exactly equal | | `raise_error(OutOfStockError, /A1/)` | that class, message matching the pattern | | `raise_error("out of stock: A1")` or `raise_error(/A1/)` | any exception with that message | | `raise_error(OutOfStockError).with_message(/A1/)` | same as the two-argument form | | `raise_error` (no argument) | any exception at all | Class matching uses the class's `===`, which is why subclasses match. A String message is compared with equality, so a message that grows a suffix (`"out of stock: A1 (warehouse 3)"`) fails an exact-string expectation; a Regexp is more robust. ## Inspecting the exception When the error carries data, pass a block to the matcher. It receives the rescued exception after the class and message have matched: ```ruby expect { order.checkout! }.to raise_error(OutOfStockError) { |error| expect(error.sku).to eq("A1") } ``` Braces and `do ... end` both work here: a `do ... end` block binds to `to`, and `to` forwards it to the matcher, which treats it as the exception handler. ## How a raise_error spec passes by accident 1. **Bare `raise_error`.** It matches every exception. If the spec calls `order.chekout!`, Ruby raises `NoMethodError`, the matcher rescues it, and the example passes without touching the checkout code. RSpec prints a warning about false positives in this case; `RSpec::Expectations.configuration.on_potential_false_positives` controls it and accepts `:warn` (the default), `:raise` or `:nothing`. 2. **Too broad a class.** `raise_error(StandardError)` accepts almost any application error, including one raised by a broken fixture. Name the class the code promises. 3. **Negative with a class.** `expect { order.checkout! }.not_to raise_error(OutOfStockError)` passes when a different error is raised, so it hides crashes. RSpec warns here too. Write `not_to raise_error` without arguments, which fails on any exception and prints it. ## The false-positive guard The warnings above go through one setting, `RSpec::Expectations.configuration.on_potential_false_positives`: | Value | Effect | |---|---| | `:warn` (default) | prints a warning and lets the expectation stand | | `:raise` | raises, so the risky expectation fails the example | | `:nothing` | silent | Switching an existing suite to `:raise` is a quick audit: every bare `raise_error` and every negative with a class shows up as a failure to fix. Passing a block to a bare `raise_error` also silences the warning, because the block is expected to inspect the error itself. `raise_exception` is an alias of `raise_error`; the two are interchangeable. ## What to assert, and what not - Assert the **class** the caller is meant to rescue; that is the contract. - Assert the **message** only where the message is part of the contract, and prefer a Regexp on the stable part. - Assert attached data through the block when callers depend on it. - Do not assert on backtraces or on exceptions the code merely lets through from a dependency.
- What does `expect { order.checkout! }.not_to raise_error` do when checkout! raises?It fails and prints the raised exception, including its class and message, because the negative form with no arguments rejects every exception. That is why it is the recommended negative, unlike `not_to raise_error(SomeClass)`, which passes when a different error is raised.
- Does `raise_error(OutOfStockError)` match an error class that inherits from OutOfStockError?Yes. The matcher tests the class with `===`, which for a class means `is_a?`, so any subclass matches. Assert the subclass itself when the distinction matters to callers.
A bare raise_error is a smoke alarm in a test kitchen: it rings for burnt toast as readily as for the dish you meant to test, so a ring alone proves nothing about the recipe.
saying these in an interview costs you the question
- expect(order.checkout!).to raise_error works the same as the block form
- a String message only needs to appear somewhere in the error message
- a bare raise_error is safe because RSpec checks the error came from the tested method
- not_to raise_error(OutOfStockError) proves the call succeeded
- raise_error(StandardError) is the most robust choice