skip to content

With Minitest, why is assert_equal clearer than assert(a == b), and when do assert_nil, assert_predicate or assert_in_delta fail more clearly?

level: juniorimportance: should knowfreq 58%

answer

  1. "Expected false to be truthy."
  2. expected first, actual second
  3. assert_nil: nil is refused by assert_equal
  4. assert_predicate obj, :empty?
  5. assert_in_delta, default delta 0.001

basics

~20 s

A bare assert only knows it got false; assert_equal expected, actual prints both values or a diff. Minitest 6 rejects a nil expected value, so use assert_nil, and assert_predicate, assert_includes and assert_in_delta name what was wrong.

solid answer

~40 s

`assert calc.fine_for(3) == 75` fails with `Expected false to be truthy.`, which says nothing about the value. `assert_equal 75, calc.fine_for(3)` fails with `Expected: 75` / `Actual: 60`, or a unified diff for long or multi-line values, so the argument order matters: **expected first**. In Minitest 6, `assert_equal nil, x` always fails with `Use assert_nil if expecting nil.`, so nil checks use `assert_nil`. Each specialised assertion prints the object it inspected: `assert_predicate member, :suspended?` reports `Expected #<Member ...> to be suspended?`, `assert_includes`, `assert_empty`, `assert_kind_of` and `assert_match` do the same, and every `assert_*` has a `refute_*` twin. Floats go through `assert_in_delta` (default delta `0.001`) or `assert_in_epsilon`. When no built-in fits, write a custom assertion on top of `assert` with a lazy `message` block.

code

ruby · 10 lines
ruby
def test_waiver_and_fine
  member = Member.new(status: :suspended)

  assert_nil calc.waiver_for(member)          # not assert_equal nil, ...
  assert_predicate member, :suspended?
  assert_operator calc.fine_for(400), :<=, 1_000
  assert_includes calc.reasons_for(member), :overdue
  assert_in_delta 0.25, calc.daily_rate_dollars, 0.001
  refute_empty calc.history_for(member)
end

go deeper

for a junior

Recall the argument order of assert_equal, that nil checks use assert_nil, and that every assert_ has a refute_ twin.

for a middle

Explain how each assertion builds its message, when Minitest prints a diff, and why floats need assert_in_delta or assert_in_epsilon.

for a senior

Show that you write assertions for the person reading a red CI log, and extract repeated domain rules into custom assertions built on assert.

for a principal

Weigh a shared library of custom assertions against their cost: a clearer vocabulary for the suite versus indirection every newcomer must learn.

## Why the choice of assertion matters An assertion exists twice: once when it passes, where every form is equivalent, and once when it fails, where the message is all you have to diagnose a red build. Minitest's assertions live in `Minitest::Assertions`, which `Minitest::Test` includes, and each one builds its failure message from the values it was given. The more specific the assertion, the more it can say. ## `assert` versus `assert_equal` `assert test, msg = nil` fails unless `test` is truthy. Given a boolean, its default message is `Expected false to be truthy.` - the values that produced the `false` are gone. `assert_equal exp, act, msg = nil` fails unless `exp == act`. Its message shows both sides: ```ruby assert_equal 75, calc.fine_for(3) # Expected: 75 # Actual: 60 ``` When either value's `inspect` output is longer than 30 characters or spans lines, or when both render identically, Minitest shells out to `diff -u` (if one is installed) and prints `--- expected` / `+++ actual`. If there is no visible difference yet the values are unequal, it tells you to suspect the class's `==` or its `inspect`. `Minitest::Test.make_my_diffs_pretty!` switches the output to `pretty_inspect` for nested hashes and arrays. Two consequences: - **Order matters.** Minitest labels the first argument `Expected` and the second `Actual`; swap them and the message lies to whoever reads it. - **`assert_equal` uses `==`.** `assert_equal 1, 1.0` passes because `1 == 1.0`; if identity matters, `assert_same` compares with `equal?`. ## nil gets its own assertion Since Minitest 6.0, `assert_equal nil, value` is refused outright: it fails with `Use assert_nil if expecting nil.` **even when `value` is nil**. Minitest 5.10 through 5.27 only warned. Since 6.0.3 `assert_same nil, value` is refused the same way. The fix is `assert_nil calc.waiver_for(member)`, whose failure reads `Expected 150 to be nil.`, and `refute_nil` for the opposite. ## The specialised family | assertion | passes when | failure message shows | |---|---|---| | `assert_nil obj` | `nil == obj` | the non-nil value | | `assert_predicate obj, :suspended?` | `obj.suspended?` is truthy | the object and the predicate | | `assert_operator fine, :<=, cap` | `fine <= cap` | both operands and the operator | | `assert_includes list, item` | `list.include?(item)` | the collection and the item | | `assert_empty coll` | `coll.empty?` | the collection | | `assert_kind_of Numeric, x` | `x.kind_of?(Numeric)` | the value and its actual class | | `assert_match(/overdue/, notice)` | `matcher =~ obj` | pattern and string; returns the `MatchData` | | `assert_in_delta 0.3, x, 0.001` | `(exp - act).abs <= delta` | the difference and the delta | Every one has a `refute_` counterpart (`refute_includes`, `refute_predicate`, `refute_empty` ...), which is clearer than `assert !x` for the same reason. Each of them accepts an optional last `msg` argument that is prepended to the generated text, so `assert_equal 75, fine, "three days late"` keeps the values and adds context. **Computed floats** rarely belong in `assert_equal`: `0.1 + 0.2 == 0.3` is false. `assert_in_delta exp, act, delta = 0.001` checks an absolute tolerance and `assert_in_epsilon exp, act, epsilon = 0.001` a relative one. For money, the better fix is often to compute in integer cents. ## Writing a custom assertion When a domain rule repeats across tests, wrap it: ```ruby module FineAssertions def assert_capped(calc, days, msg = nil) msg = message(msg) { "Expected fine for #{days} days to stay within the cap" } assert_operator calc.fine_for(days), :<=, calc.cap_cents, msg end end class FineCalculatorTest < Minitest::Test include FineAssertions end ``` The rules that keep it behaving like a built-in: 1. Build on `assert` or another `assert_*`, so the assertion counter increments and a failure raises `Minitest::Assertion` (a Failure), not your own error class (an Error). 2. Use `message(msg) { ... }` so the text is computed only on failure and a caller's message is prepended. 3. Start the name with `assert_` or `refute_` and take an optional trailing `msg`, matching the house style. Note the 6.0 change: if `msg` is itself a proc, it now **replaces** the generated text instead of being chained with it. ## Summary - Prefer the most specific assertion that states the rule. - Expected first, actual second. - `assert_nil` for nil, `assert_in_delta` for floats. - Custom assertions for repeated domain rules, built on `assert`.

  • With Minitest, what happens if you swap the arguments of assert_equal?
    The comparison still uses `==`, so pass and fail do not change, but the failure message labels the first argument `Expected` and the second `Actual`. Swapped, the report claims the code returned the value you meant to expect, which sends whoever reads the red build in the wrong direction.
  • With Minitest, when should a test use assert_same instead of assert_equal?
    When identity matters: `assert_same` checks `exp.equal?(act)`, the same object, and prints both object ids on failure, for example to prove a memoised method returns the cached instance. `assert_equal` checks `==` and passes for two equal but distinct objects. In Minitest 6.0.3 and later, `assert_same nil, x` is refused; use `assert_nil`.

saying these in an interview costs you the question

  • assert(fine == 75) reports the same failure detail as assert_equal.
  • assert_equal takes the actual value first and the expected value second.
  • In Minitest 6, assert_equal nil, value passes when value is nil.
  • assert_equal 0.3, 0.1 + 0.2 is fine for float results.
  • A custom assertion should raise its own error class when the rule fails.