Lest is a testing toolchain for Luau: a single binary that carries the test
framework inside it. You write describe / it / expect once, and the same
suites run as pure logic, as Lune or
Lute scripts, or against real Roblox engine
APIs in an actual place. One command, one test API, any environment.
--!strict
local Lest = require('@lest')
local describe, it, expect = Lest.describe, Lest.it, Lest.expect
local cart = require('./cart')
describe('cart', function ()
it('sums line items', function ()
expect(cart.total({ 3, 4 })).toBe(7)
end)
it('rejects a negative quantity', function ()
expect(function ()
cart.total({ -1 })
end).toThrow('quantity')
end)
end)
return nil$ lest
unit (native)
cart
✓ sums line items (0.0ms)
✓ rejects a negative quantity (0.1ms)
Slowest Tests:
cart › rejects a negative quantity (0.1ms)
cart › sums line items (0.0ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.01s-
Nothing to install alongside it. The framework is compiled into the binary, not a package you add to your project. The runner and the framework are always the same version and can never disagree about the protocol between them.
-
Zero configuration. No
lest.toml? Every**/*.spec.luauruns on the embedded VM. A real config for a real project is two lines. -
Four environments, one report. Pure logic runs on an embedded Luau VM;
@lune/*and@lute/*scripts run in the actual runtimes; engine code runs in a real Roblox place through Open Cloud. Results merge into a single report. -
No engine emulation. Ever. Nothing here mocks Instances or fakes a runtime. Partial mocks produce confident wrong tests — if a test needs an environment, Lest runs it in that environment.
-
Built for the save-run loop. Watch mode re-runs only the specs whose transitive requires actually changed.
-
Snapshots, coverage, and CI output.
toMatchSnapshotwith-uto update, line coverage with lcov and a--mingate, JUnit XML, and exit codes that never conflate a failing test with a broken tool.
Install with LPM, which pins the version per project and puts lest on your PATH:
$ lpm tool add lest
$ lpm iYou can also install with rokit:
$ rokit add lest-luau/lestOr build from source with Rust:
$ git clone https://github.com/lest-luau/lest
$ cd lest
$ cargo build --release
$ ./target/release/lest self install # copies it into ~/.lest/bin, adds it to PATHThen, in your project:
$ lest initlest init detects what it can (a rojo project file, lune/lute on PATH,
existing spec files) and asks only about what it can't infer. It writes a
commented lest.toml, an example spec, and — if you accept — a .luaurc alias
so specs can require('@lest') from anywhere. Pass --yes to take every
default without prompting.
Now write a spec and run it:
$ lest # every default suite
$ lest run unit # one suite
$ lest --watch # re-run affected specs on save
$ lest -t 'cart' # only tests whose full name contains "cart"Full walkthrough: Getting started.
A suite's backend is where its specs actually execute. You choose it once in
lest.toml; everything downstream — reporters, snapshots, filters, CI output —
neither knows nor cares where a test ran.
| Backend | Runs in | Use it for | Coverage | Watch |
|---|---|---|---|---|
native |
An embedded Luau VM inside the CLI | Pure logic. The default, and the fast one | ✅ | ✅ |
lune |
A spawned lune run process |
Scripts using @lune/* |
— | ✅ |
lute |
A spawned lute run process |
Scripts using @lute/*, code transforms, tooling |
— | ✅ |
cloud |
A real Roblox place via Open Cloud | Instances, services, the DataModel — in CI | — | — |
studio |
A launched Roblox Studio | The same engine code, locally, zero clicks | — | — |
[suites.unit]
include = ["src/**/*.spec.luau"]
[suites.engine]
include = ["tests/engine/**/*.spec.luau"]
backend = "cloud"
default = false # opt in locally; auto-enabled when $CI is set
[place] # the Roblox place engine suites run in
universe_id = 1234567890
place_id = 9876543210More: Backends.
| Guide | |
|---|---|
| Getting started | Install, scaffold, and run your first suite |
| Writing tests | describe, it, lifecycle hooks, skipping |
| Matchers | Every matcher, with semantics and failure output |
| Backends | Native, Lune, Lute, and Open Cloud |
| Studio | Engine suites in a launched Roblox Studio |
| Configuration | Every lest.toml key |
| CLI reference | Every command and flag, including watch mode |
| Snapshots | toMatchSnapshot, updating, obsolete keys |
| Coverage | Line coverage, lcov, and the --min gate |
| Continuous integration | Exit codes, JUnit, --changed, cloud in CI |
| Contributing | Repository layout and the development loop |
Issues and pull requests are welcome. Contributing covers the repository layout, the toolchain, and how Lest tests itself.
Lest is MIT licensed.
