Assertions¶
Every assertion starts with Wisp.assume(value), which wraps a value and returns an
Assertion. You then call exactly one method on it:
assume accepts any value — including nil, false, and 0 — and it never
evaluates the value for truthiness. There is no separate "truthy" assertion, because in
Luau most interesting values are truthy.
Alias it
local assume = Wisp.assume at the top of a spec file shortens every line and makes
the assertions read as the subject of the test.
How failures work¶
Assertion methods do not return a boolean. They either return nothing (pass) or call
error(message, 2) (fail). Wisp catches that error in Suite:run and attributes it to
the action that raised it.
Because the level is 2, the reported location is your assertion line, not a line inside
Wisp.
At a glance¶
| Method | Passes when |
|---|---|
equalsTo |
The value equals the expected value (~= is false). |
notEqualsTo |
The value differs from the expected value. |
isTrue |
The value is exactly true. |
isFalse |
The value is exactly false. |
isNil |
The value is nil. |
isNotNil |
The value is not nil. |
isNaN |
The value is NaN. |
isInf |
The value is positive or negative infinity. |
isFinite |
The value is a finite number. |
isType |
typeof(value) equals the given type name. |
throws |
The value is a function, and calling it raises. |
notThrows |
The value is a function, and calling it returns normally. |
contains |
A string contains the substring, or a table contains the value. |
notContains |
The inverse of contains. |
Equality¶
equalsTo¶
Wisp.assume(1):equalsTo(1)
Wisp.assume("wisp"):equalsTo("wisp")
Wisp.assume(true):equalsTo(true)
Wisp.assume(nil):equalsTo(nil)
Fails with Assertion failed: expected 2, got 1.
Equality is ==, not deep equality
Tables and functions are compared by identity, so two structurally identical tables fail:
local config = { retries = 3 }
Wisp.assume(config):equalsTo(config) -- passes, same reference
Wisp.assume({ retries = 3 }):equalsTo({ retries = 3 }) -- fails
To compare contents, assert on the fields you care about:
NaN is also never equal to itself, so assume(math.nan):equalsTo(math.nan) fails.
Use isNaN instead.
notEqualsTo¶
Fails with Assertion failed: expected not 1, got 1.
Booleans¶
isTrue¶
Passes only for the value true. 1, "true", and any truthy non-boolean fail.
Wisp.assume(true):isTrue() -- passes
Wisp.assume(false):isTrue() -- Assertion failed: expected true, got false
Wisp.assume(nil):isTrue() -- Assertion failed: expected true, got nil
isFalse¶
Passes only for the value false. 0 and nil fail.
Wisp.assume(false):isFalse() -- passes
Wisp.assume(true):isFalse() -- Assertion failed: expected false, got true
Wisp.assume(nil):isFalse() -- Assertion failed: expected false, got nil
Nil¶
isNil¶
Wisp.assume(nil):isNil() -- passes
Wisp.assume(false):isNil() -- Assertion failed: expected nil, got false
Wisp.assume(0):isNil() -- Assertion failed: expected nil, got 0
isNotNil¶
The inverse. Note that every other falsy value passes, since only nil is nil:
Wisp.assume(false):isNotNil() -- passes
Wisp.assume(0):isNotNil() -- passes
Wisp.assume(""):isNotNil() -- passes
Wisp.assume(nil):isNotNil() -- Assertion failed: expected non-nil, got nil
Numbers¶
isNaN¶
Wisp.assume(0 / 0):isNaN() -- passes
Wisp.assume(1):isNaN() -- Assertion failed: expected nan, got 1
Wisp.assume(math.huge):isNaN() -- Assertion failed: expected nan, got inf
isInf¶
Matches both signs.
Wisp.assume(math.huge):isInf() -- passes
Wisp.assume(-math.huge):isInf() -- passes
Wisp.assume(1):isInf() -- Assertion failed: expected inf, got 1
Wisp.assume(0 / 0):isInf() -- Assertion failed: expected inf, got nan
isFinite¶
Passes for ordinary numbers, including 0 and negatives; fails for both infinities and
NaN.
Wisp.assume(0):isFinite() -- passes
Wisp.assume(-1.5):isFinite() -- passes
Wisp.assume(math.huge):isFinite() -- Assertion failed: expected a finite number, got inf
Wisp.assume(0 / 0):isFinite() -- Assertion failed: expected a finite number, got nan
Types¶
isType¶
Compares typeof(value) against a type name string.
Wisp.assume(1):isType("number")
Wisp.assume("wisp"):isType("string")
Wisp.assume(false):isType("boolean")
Wisp.assume({}):isType("table")
Wisp.assume(print):isType("function")
Wisp.assume(nil):isType("nil")
Fails with Assertion failed: expected type string, got type number.
Because the comparison is a plain string match, a misspelled type name is reported against the actual type rather than crashing:
Errors¶
throws¶
Asserts that a function raises. The expression must be a function — anything else fails with a message telling you so.
Wisp.assume(function()
error("boom")
end):throws()
Wisp.assume(function() end):throws()
-- Assertion failed: expected function to throw, but it did not
Wisp.assume(5):throws()
-- throws() requires a function, got number
Because it uses pcall, it also matches a nested assertion failure:
The message is not checked
throws() only asserts that an error occurred, not what it said. If you need to
assert on the message, call the function yourself and check the string:
notThrows¶
The inverse. Useful as a smoke test that a call path does not error:
Wisp.assume(function()
Wisp.assume(1):equalsTo(1)
end):notThrows()
Wisp.assume(function()
error("boom")
end):notThrows()
-- Assertion failed: expected function not to throw, but it did
Wisp.assume("nope"):notThrows()
-- notThrows() requires a function, got string
Containment¶
contains¶
Works on strings and tables, and rejects everything else.
Strings — plain substring match. The needle is never treated as a Lua pattern, so
. and % are literal:
Wisp.assume("hello world"):contains("lo wo") -- passes
Wisp.assume("hello"):contains("hello") -- passes
Wisp.assume("a.c"):contains(".") -- passes
Wisp.assume("axc"):contains("a.c") -- fails: it is a literal search
Fails with Assertion failed: expected string to contain bye, but it did not.
Tables — scans every value, not just the array part:
Wisp.assume({ "a", "b" }):contains("b") -- passes
Wisp.assume({ key = "value" }):contains("value") -- passes
Wisp.assume({ "a", extra = "b" }):contains("b") -- passes
Wisp.assume({}):contains("z") -- fails
Fails with Assertion failed: expected table to contain z, but it did not.
Anything else raises immediately:
notContains¶
The exact inverse, including the "unsupported type" behaviour:
Wisp.assume("hello"):notContains("bye") -- passes
Wisp.assume("hello"):notContains("ell") -- fails: it is present
Wisp.assume({ "a" }):notContains("z") -- passes
Wisp.assume({ "a", "b" }):notContains("b") -- fails: it is present
Wisp.assume({}):notContains("z") -- passes
Wisp.assume(true):notContains(1)
-- notContains() not supported for type boolean
Values only, and == only
contains on a table inspects values, never keys, and uses ==. It will not find
a key, will not match nested tables by content, and — like equalsTo — cannot find
NaN. For key or shape checks, use rawget or isType:
Reading failure messages¶
Messages are built with tostring, which means two different values can produce the same
text:
The failure is real — 1 ~= "1" — but the message cannot tell you which is which. When a
message looks confusing, add an isType assertion first to pin the type down.
Writing good assertions¶
Prefer asserting the specific value over asserting on a container.
Keep one behaviour per action. Because an action stops at its first failed assertion, grouping several assertions hides everything after the first failure.
Next¶
- Reading the report — turning failures into fixes.
- Assertion API reference — signatures and error strings.