Skip to content

Writing Your First Test

This walkthrough builds a small test file from scratch. Every snippet is the real thing — it is the same shape as Wisp's own test suite.

1. Require Wisp

tests/run.luau
local Wisp = require("../src")

2. Create a suite and add actions

A suite is a named group of tests. An action is one test inside it. Suite:action(name, func) takes a human readable name and a function with no arguments and no return value.

Wisp.suite("Calculator")
  :action("adds two numbers", function()
    -- assertions go here
  end)

Suite:action returns the suite itself, which is why the calls above chain. Without chaining you would have to keep a reference:

local calculator = Wisp.suite("Calculator")
calculator:action("adds two numbers", function() end)

3. Assert something

Wisp.assume(value) wraps a value and returns an Assertion. Each assertion method either passes silently or raises, so there is no return value to check.

Wisp.suite("Calculator")
  :action("adds two numbers", function()
    Wisp.assume(1 + 1):equalsTo(2)
  end)
  :action("divides", function()
    Wisp.assume(10 / 2):equalsTo(5)
  end)

For readability, alias assume once at the top of the file:

local Wisp = require("../src")
local assume = Wisp.assume

Wisp.suite("Calculator")
  :action("adds two numbers", function()
    assume(1 + 1):equalsTo(2)
  end)

Wisp's own test suite does exactly this — see tests/Assertion.spec.luau.

4. Run it

Call the module itself. It is callable, so Wisp() runs the default runner: every registered suite, in registration order, then prints a report.

Wisp()

That is the whole runner. A complete test file looks like this:

tests/run.luau
local Wisp = require("../src")
local assume = Wisp.assume

Wisp.suite("Calculator")
  :action("adds two numbers", function()
    assume(1 + 1):equalsTo(2)
  end)
  :action("coerces a numeric string", function()
    assume("1" + 1):equalsTo(2)
  end)

return Wisp()
Calculator
 ✓ adds two numbers
 ✓ coerces a numeric string

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

Took:   0.07ms

5. Make it fail

Assertions raise on failure, so a mismatch shows up as a failed action with the message Wisp produced. Change equalsTo(2) to equalsTo(3):

Wisp.suite("Calculator")
  :action("adds two numbers", function()
    Wisp.assume(1 + 1):equalsTo(3)
  end)
  :action("divides", function()
    Wisp.assume(10 / 2):equalsTo(5)
  end)

Wisp()
Calculator
 ✗ adds two numbers
   ./run.luau:6: Assertion failed: expected 3, got 2
 ✓ divides

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

Took:   0.06ms

Note that the second action still ran. A failing assertion never stops the run — see Reading the report.

Organising multiple files

Wisp keeps one process-wide default runner, so a top-level file can pull in the rest and run them in one go:

tests/run.luau
local Wisp = require("../src")

require("./Calculator.spec")
require("./Cart.spec")
require("./Inventory.spec")

return Wisp()

Each spec file simply registers suites as a side effect of being required:

tests/Cart.spec.luau
local Wisp = require("../src")
local assume = Wisp.assume

Wisp.suite("Cart")
  :action("starts empty", function()
    assume(#{}):equalsTo(0)
  end)

return true

Suite names must be unique

Registering two suites with the same name keeps only the first — the second is silently ignored. If a suite you wrote never appears in the report, check for a duplicate name elsewhere in your project.

Next