Skip to content

Suites and Actions

A suite groups related tests; an action is a single test. Wisp has no other concept — there are no nested suites, no hooks, and no tags.

Creating a suite

local suite = Wisp.suite("Calculator")

Wisp.suite(name) takes a non-empty name and immediately registers the suite with the default runner, so it will be picked up by the next Wisp() call.

Suites are flat. If you want grouping in the output, put it in the name:

Wisp.suite("Calculator:addition")
Wisp.suite("Calculator:division")
Wisp.suite("Cart:checkout")

Adding actions

Wisp.suite("Calculator")
  :action("adds two numbers", function()
    Wisp.assume(1 + 1):equalsTo(2)
  end)
Parameter Type Description
name string Printed in the report. Make it describe the behaviour, not the mechanics.
func () -> () The test body. Called with no arguments.

Suite:action returns the suite, not the action. That is what makes the chain work:

Wisp.suite("Calculator")
  :action("first", function() end)
  :action("second", function() end)
  :action("third", function() end)
local calculator = Wisp.suite("Calculator")

calculator:action("first", function() end)
calculator:action("second", function() end)
calculator:action("third", function() end)
Wisp.suite("Calculator")
  :action("first", function() end)
  :action("second", function() end)
  :action("third", function() end)

Execution order

Actions run in the order they were added, and suites run in the order they were registered:

Wisp.suite("First"):action("a", function() end):action("b", function() end)
Wisp.suite("Second"):action("a", function() end)

Wisp()
-- First  → a, b
-- Second → a

Registration happens at the moment Wisp.suite(name) is called, not when the run starts. So the order suites appear in the report is the order those lines executed during the require phase.

Failures do not stop the run

Every action is invoked through pcall. If it raises — because an assertion failed, or because your own code threw — the error message is recorded and the next action runs:

Wisp.suite("Robust")
  :action("explodes", function()
    error("kaboom")
  end)
  :action("still runs", function()
    Wisp.assume(true):isTrue()
  end)
Robust
 ✗ explodes
   ./run.luau:4: kaboom
 ✓ still runs

Suites: 0 passed out of 1 total
Tests:  1 passed out of 2 total

This is the main behavioural difference from a plain script of error calls: one broken test does not hide the rest of the report.

Only the action's own error is caught

If the reporter raises — for example Runner:pass is called for a suite that was never registered — that error is not caught and will propagate out of Wisp().

Unique names

A runner keys its statistics by suite name. Registering a second suite with a name that is already taken is silently ignored, and that suite's actions will never run:

Wisp.suite("Duplicated")
  :action("this runs", function() end)

Wisp.suite("Duplicated")
  :action("this never runs", function()
    error("should not get here")
  end)
Duplicated
 ✓ this runs

Suites: 1 passed out of 1 total
Tests:  1 passed out of 1 total

If a suite is missing from your report, look for a name collision before anything else.

The suite object

Wisp.suite returns a live Suite instance. It is rarely useful, but it is public API — see Suite for the full reference, including how to construct one against your own runner.

Next