1 Assertions
dxtr edited this page 2026-10-18 12:07:40 +02:00

Assertions

All assertions record and continue — a failure does not end the test, so one run tells you about every broken thing at once (Go's t.Error, JUnit's assertAll). Only must stops the test.

Form Meaning
(is form [message args...]) Record the result and continue. On failure, show the operands.
(must form [message args...]) Like is, but abort the test (or subtest) on failure.
(signals type form &key matching) form must signal a condition of type; returns that condition.
(finishes form) form must return without an error; returns its value.
(fail control args...) Record a failure unconditionally, with a formatted message.
(skip &optional reason) End the test with status :skip.
(note control args...) Attach a note, shown only if the test fails or errors.
(abort-test) End the innermost test or subtest now.
(approx= a b &key tolerance) `

Message arguments are evaluated only on failure, so (is x "got ~a" (expensive)) costs nothing when it passes.

How is reads its form

is takes the macro environment and classifies its form:

  1. A function-call form — (op arg...) where op is a symbol that is neither a special operator nor a macro. Each non-literal argument is bound to a fresh variable and evaluated once, left to right, so evaluation count and order are exactly the source's. On failure every non-literal operand is shown with its value, and explain is asked for a line.
  2. (not x) is classified by classifying x, with the result negated. The shown operands are x's.
  3. Anything else — an atom, a macro call, a special form, a lambda form — is opaque: it is evaluated once and the whole value is shown.

A literal is a constant atom, or a quote, function or lambda form. Literals are evaluated in place and not shown. So in (is (= -1 (parse "-1"))) the -1 is not shown, but (parse "-1") is.

(let ((a 0))
  (is (= (incf a) (incf a))))   ; a ends at 2; the operands are shown as 1 and 2

signals

type is an unevaluated type specifier. The form runs under handler-bind; the first condition of type — any condition, including a warned warning — ends it, and signals returns that condition so you can inspect it.

  • If the form returns normally, the assertion fails: expected TYPE to be signaled, but the form returned normally.
  • If an error of another type reaches the handler, the assertion fails (expected TYPE, got OTHER-TYPE: report) and returns NIL; the error does not become a test error, and the next assertion still runs.
  • With :matching "text", the assertion also fails when the condition's report does not contain the text.
(signals parse-error (parse "4x2"))
(signals error (/ 1 0) :matching "DIVISION-BY-ZERO")
(let ((c (signals my-error (do-thing))))
  (is (= 42 (error-code c))))

finishes

Passes when the form returns normally, and returns its primary value. An error fails the assertion (signaled TYPE: report) and the test continues; a non-error condition is not caught.

(is (= 3 (finishes (+ 1 2))))

fail, skip, note, abort-test

  • fail records a failed assertion whose message is the formatted text.
  • skip sets the status to :skip with the reason and ends the test; assertions recorded before it are kept but not shown.
  • note attaches text that appears under notes: only when the test fails or errors — handy for logging the input that produced a failure.
  • abort-test ends the innermost test or subtest, and is safe to call from a helper function.

At the REPL

Outside any run, is, must, fail and note print what they would have recorded, so you can try the framework without writing a test:

CL-USER> (is (= 1 1))
✓ (is (= 1 1))

CL-USER> (is (string= "abc" "abd"))
✗ (is (string= "abc" "abd"))
      differ at index 2: "abc" vs "abd"

is and must return the predicate's primary value; skip and abort-test do nothing and return NIL.

See Explain for the failure lines, and Output Format for the exact layout.