TestEZ Complete Setup Guide — Adding Unit Testing to Your Roblox/Luau Project [2026]
TestEZ is Roblox's official Luau unit testing framework. This guide walks you through setup with Wally, Rojo, and Selene step by step.
What is TestEZ? — Roblox's Official Luau Unit Testing Framework
TestEZ is an open-source unit testing framework for Luau, developed and maintained by Roblox. It uses a BDD-style API (describe/it/expect) similar to Jasmine and Mocha, and is used internally by Roblox itself — making it one of the most battle-tested tools in the ecosystem.
Why Add TestEZ to Your Project?
Integrating TestEZ brings four key benefits: (1) Regression prevention — instantly verify that existing features still work after changes. (2) Safe refactoring — tests give you confidence to reorganize code aggressively. (3) Living documentation — test cases serve as human-readable specifications. (4) CI integration — automate dependency installation via GitHub Actions to maintain consistent quality across teams.
Prerequisites
Before starting, make sure you have: Roblox Studio (latest), Rojo (bidirectional file sync between VSCode and Studio), Rokit (tool version manager), Wally (Luau package manager), and Selene (static analysis linter). If any of these are missing, refer to their respective GitHub repositories for installation instructions.
Step 1: Set Up Wally
Add Wally via Rokit:
# Add Wally with Rokit
rokit trust UpliftGames/wally
rokit add UpliftGames/wally
wally --versionIf a version number is printed, the installation is complete.
Step 2: Add TestEZ to wally.toml
Add a `[dev-dependencies]` section to your `wally.toml`:
[package]
name = "your-name/your-project"
version = "0.1.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
[dev-dependencies]
TestEZ = "roblox/testez@0.4.1"Run `wally install` to generate `DevPackages/TestEZ.lua` automatically.
Step 3: Map DevPackages in Your Rojo Project
Update `default.project.json` to expose DevPackages inside ReplicatedStorage:
"ReplicatedStorage": {
"$path": "src/ReplicatedStorage",
"DevPackages": {
"$path": "DevPackages"
}
}This allows requiring TestEZ from `ReplicatedStorage.DevPackages.TestEZ` in Studio.
Step 4: Register TestEZ Globals in Selene
Create `testez.yml` at the project root so Selene recognizes describe/it/expect and lifecycle hooks:
globals:
describe:
args:
- type: string
- type: function
it:
args:
- type: string
- type: function
expect:
args:
- type: any
beforeAll:
args:
- type: function
beforeEach:
args:
- type: function
afterEach:
args:
- type: function
afterAll:
args:
- type: functionThen set `std = "roblox+testez"` in `selene.toml`.
Step 5: Configure .gitignore
Exclude Wally-generated packages from version control:
Packages/
DevPackages/
wally.lockKeeping `wally.lock` out of git avoids environment-specific conflicts.
Recommended Directory Structure
| Path | Purpose |
|---|---|
| `wally.toml` | Package definitions |
| `testez.yml` | TestEZ globals for Selene |
| `DevPackages/` | Generated by wally install (not in git) |
| `src/ServerScriptService/Tests/` | Test file directory |
| `Tests/RunTests.server.luau` | Test runner script |
| `Tests/*.spec.luau` | Individual test files |
Creating the Test Runner
Create a server script that auto-runs on F5 Play:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage:WaitForChild("DevPackages"):WaitForChild("TestEZ"))
-- Recursively find all *.spec modules inside Tests/
local specs = {}
local testsFolder = script.Parent
for _, descendant in ipairs(testsFolder:GetDescendants()) do
if descendant:IsA("ModuleScript") and descendant.Name:match("\.spec") then
table.insert(specs, descendant)
end
end
local results = TestEZ.TestBootstrap:run(specs, TestEZ.Reporters.TextReporter)Basic Test Structure
Name test files `<module>.spec.luau` and follow this pattern:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MyModule = require(ReplicatedStorage:WaitForChild("Modules"):WaitForChild("MyModule"))
return function()
describe("myFunction", function()
it("returns 3 when given 1 and 2", function()
local result = MyModule.myFunction(1, 2)
expect(result).to.equal(3)
end)
end)
endCommon Pitfall: expect Is Only Valid Inside `return function()`
TestEZ injects describe/it/expect dynamically when `return function()` is called. Defining a helper outside this scope and using expect inside it will cause `attempt to call a nil value`. BAD pattern:
-- BAD: expect is nil outside return function()
local function helper(v)
expect(v).to.equal(1) -- nil here!
end
return function()
it("test", function() helper(1) end)
endGOOD pattern:
-- GOOD: define helper inside return function()
return function()
local function helper(v)
expect(v).to.equal(1) -- injected here
end
it("test", function() helper(1) end)
endCommon Assertion APIs
| API | Purpose |
|---|---|
| `expect(v).to.equal(x)` | Equality check |
| `expect(v).to.be.ok()` | Verify not nil |
| `expect(v).never.to.equal(x)` | Negation |
| `expect(typeof(v)).to.equal("string")` | Type check |
| `expect(fn).to.throw()` | Expect exception |
| `expect(t).to.be.a("table")` | Type assertion |
Lifecycle Hooks
| Hook | When It Runs |
|---|---|
| `beforeAll(fn)` | Once before all tests in a describe block |
| `beforeEach(fn)` | Before each it block |
| `afterEach(fn)` | After each it block |
| `afterAll(fn)` | Once after all tests in a describe block |
How to Run Tests
(1) Run `rojo serve` in your terminal. (2) Click Connect in the Rojo plugin inside Roblox Studio. (3) Press F5 to start Play mode. (4) Open the Output window to check test results. Note that the Output window has no default keyboard shortcut on either Mac or Windows — open it via the View tab in the menu bar, then click Output (you can optionally assign a custom shortcut via File → Advanced → Customize Shortcuts) to view results. Sample output:
[TestEZ] 3 passed, 0 failed, 0 skippedCI Integration — Automating with GitHub Actions
You can automate the `wally install` step in GitHub Actions. Full test execution still requires Roblox Studio, but headless automation via `run-in-roblox` is an emerging option for teams that want fully automated pipelines.
- name: Verify Wally
run: wally --version
- name: Install Wally packages
run: wally installFAQ — Frequently Asked Questions About TestEZ
Q: Is TestEZ free to use? A: Yes. TestEZ is released under the MIT License and is completely free for both personal and commercial projects. Q: Can I run TestEZ outside of Studio? A: The official setup assumes Roblox Studio. However, `run-in-roblox` enables headless execution for CI pipelines. Q: Are type annotations required? A: They are not required, but using Luau Strict Mode alongside TestEZ helps catch bugs earlier and is strongly recommended. Q: Is the API similar to Jest or Mocha? A: Yes. The BDD-style describe/it/expect pattern is nearly identical, so developers with JavaScript testing experience will feel right at home. Q: Can I test async code? A: Yes, by combining TestEZ with a Roblox-compatible Promise library such as evaera/promise. Q: Is code coverage measurement supported? A: Not natively. TestEZ does not include a built-in coverage reporter; a separate tooling solution is required. Q: How do I share helper functions across multiple spec files? A: Extract helpers into a ModuleScript and require it from each spec file. Remember to define any functions that use expect inside the `return function()` closure.
Oflight Can Help You Build Quality Roblox/Luau Projects
Oflight provides hands-on technical support for Roblox development — from setting up TestEZ and CI pipelines to Luau refactoring and test design consultations. If you want to improve your team's development quality and productivity, we'd love to help. View Our Software Development Services
Feel free to contact us
Contact Us