Skip to content

Luau Run · Guides

Writing tests

To start off, this page only covers how Luau Run finds and runs your tests. It is not a general guide to writing good tests.

luau-run test runs two kinds of file under the folders in roots (tests unless set):

  • *.spec.luau is a Jest spec
  • *.test.luau is a script test

No other name counts, not even *.spec.lua. Hidden folders, files a .gitignore ignores, and folders such as node_modules are skipped. Which files are found has the full rules. A path on the command line replaces roots for that run.

Jest specs run on Jest-Portable, Luau Run’s version of Jest Roblox that also runs on Lute.

A Jest spec takes Jest from require("@jest/globals"), as the example in Getting started does. On Lute, describe, it and expect are also globals.

A spec has task, delay, tick, time and DateTime, which Jest’s fake timers control. On Lute they also control a spec’s @lute/task and @std/task, and jest.requireActual still gets the real library. Nothing else is added: a bare Promise or game is nil on Lute.

On Lute, a spec that requires a project’s own copy of Jest’s globals by another path, such as @pkg/@jsdotlua/jest-globals, loads that copy instead of Jest-Portable. Only the Studio lane swaps the copy for Jest-Portable.

Snapshots cannot be written: toMatchSnapshot fails for a snapshot that does not exist.

A *.test.luau file runs on Lute in its own process, from the project’s folder, and passes when it finishes without an error. A failure names its file and line:

FAIL tests/tools/Codec.test.luau:3
Failure: a round trip keeps the third value

When a script registers cases with Lute’s @std/test, Luau Run reports each case, as lute test does. A script test that returns a function fails, since nothing would call it. To run your own test kit, see Custom runners.

A script test ends when its file finishes. Luau Run does not wait for tasks the file leaves behind, or report their errors. Waiting for them is on the roadmap.

Luau Run refuses a script test whose full path is 260 characters or more, since Lute on Windows cannot open it. Move the project to a shorter folder, or shorten the path.

Lute is not the luau command-line tool: it has no collectgarbage, getfenv, setfenv or loadstring.

On Lute, script tests and Jest specs get the environment variables Luau Run was started with, PATH included. Luau Run overrides a few, such as TEMP and TMP, and sets UV_THREADPOOL_SIZE to 1.

Each file gets a runtime, lute or roblox. The runtime and the file’s runner decide which lane runs it, such as Jest specs on Lute or Jest specs in Studio. The runtime is picked in order of priority:

  1. a header at the top of the file, before any code:

    -- luau-run: runtime=roblox
    --!strict
  2. --runtime on the command line

  3. the last [[test.runtime-rules]] entry whose glob matches the file

  4. the runtime of the custom runner a runner rule gives the file

  5. default-runtime under [test]

A [[test.runner-rules]] entry changes the runner for the files its glob matches: jest, script, or a runner the project declares. Both kinds of rule look like this:

[[test.runner-rules]]
glob = "src/**/*.test.luau"
runner = "jest"
[[test.runtime-rules]]
glob = "tools/**"
runtime = "lute"

--log-warnings shows where rules disagree and where a header was ignored.

© 2026 PRMS Developer