alloy test
UpdatedA test lives beside the code it tests: a @test function in the module, blanked from the ship artifact. @test on a namespace marks the whole group, so every public function of it is a test named Suite.name. alloy test builds the project, then writes one lest spec per source that holds a @test, under [test] out:
src/inventory.aly -> tests/inventory.spec.luau src/ui/menu.alx -> tests/ui/menu.spec.luau src/loot.spec.aly -> tests/loot.spec.luau
A spec carries the tests and every top-level statement they reach: the imports they use, the locals and functions they call, the structs and the impls those need. The rest of the module stays out, so its side effects stay out of the test VM. An import that only tests read leaves the ship artifact with them. An import inside a test requires its module on the first line of the spec, because lest's native backend resolves a require only while the spec loads. The slice keeps the source's lines, so a failure points at the real one. Each relative require in the spec points at the build output, the runtime included, and the spec ends with a describe that registers each test by name; an async test is awaited.
local function clamp01(x: number): number return math.clamp(x, 0, 1) end @test function clamp_keeps_range() $assert_eq(clamp01(2), 1) end
$expect(value) gives the runner's expectation object, so $expect(hp):toBe(100) and $expect(open_chest):toThrow() read as they do in jest. It needs @test on the function or the namespace around it, and it needs [test] lest on; alloy doc $expect lists what the matchers take.
alloy test build, then write the specs alloy test --run and run lest on the suite alloy test --coverage run lest with line coverage alloy test --filter hit run the tests whose name holds `hit` alloy test -- --reporter json the rest goes to lest as given alloy test --watch write again after every change alloy test --check write nothing; fail when a spec would change alloy test src/game.aly one file's spec, to stdout
The [test] table:
[test] out = "tests" # where the specs go suite = "alloy" # the suite name in lest.toml lest = true # write lest.toml and the @lest alias when the root has none
With lest = true, the first run writes a lest.toml with one suite over tests/**/*.spec.luau on the native backend, and adds lest = ".lest/core" to the aliases of the project's Luau configuration, the .config.luau or the .luaurc. That folder is where lest puts its framework. lest then runs the suite; on its VM the runtime falls back to plain coroutines. With shim = true, a task.wait parks its thread, and the spec steps the parked threads until an async test's Future settles, so an await in the test returns. A test whose Future no parked thread settles fails with that message. $assert, $assert_eq, and a matcher that fails raise, and lest reports the line.
