Skip to content

Assertions

Every assertion starts with Wisp.assume(value), which wraps a value and returns an Assertion. You then call exactly one method on it:

Wisp.assume(2 + 2):equalsTo(4)
Wisp.assume(value)   -->  assertion.method(...)

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.

Wisp.assume(1):equalsTo(2)
-- error: Assertion failed: expected 2, got 1

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:

Wisp.assume(#config.retries):equalsTo(3)
Wisp.assume(config.name):equalsTo("prod")

NaN is also never equal to itself, so assume(math.nan):equalsTo(math.nan) fails. Use isNaN instead.

notEqualsTo

Wisp.assume(1):notEqualsTo(2)
Wisp.assume(1):notEqualsTo(nil)

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:

Wisp.assume(1):isType("numbre")
-- Assertion failed: expected type numbre, got type number

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:

Wisp.assume(function()
  Wisp.assume(1):equalsTo(2)
end):throws()   -- passes

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:

local ok, err = pcall(validate, 0)
Wisp.assume(ok):isFalse()
Wisp.assume(tostring(err)):contains("positive")

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:

Wisp.assume(5):contains(1)
-- contains() not supported for type number

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:

Wisp.assume(rawget(config, "retries")):isNotNil()
Wisp.assume(next(config)):isNotNil()   -- table has at least one key

Reading failure messages

Messages are built with tostring, which means two different values can produce the same text:

Wisp.assume(1):equalsTo("1")
-- Assertion failed: expected 1, got 1

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.

Wisp.assume(#cart.items):equalsTo(0)
Wisp.assume(cart.total):equalsTo(0)
-- Passes or fails as a whole, and tells you nothing when it fails.
Wisp.assume(cart.items):contains("apple")

Keep one behaviour per action. Because an action stops at its first failed assertion, grouping several assertions hides everything after the first failure.

Next