株式会社オブライト
Software Dev2026-04-17

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.

Loading diagram...

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:

bash
# Add Wally with Rokit
rokit trust UpliftGames/wally
rokit add UpliftGames/wally
wally --version

If 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`:

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:

json
"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:

yaml
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: function

Then set `std = "roblox+testez"` in `selene.toml`.

Step 5: Configure .gitignore

Exclude Wally-generated packages from version control:

gitignore
Packages/
DevPackages/
wally.lock

Keeping `wally.lock` out of git avoids environment-specific conflicts.

Recommended Directory Structure

PathPurpose
`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:

lua
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:

lua
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)
end

Common 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:

lua
-- 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)
end

GOOD pattern:

lua
-- 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)
end

Common Assertion APIs

APIPurpose
`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

HookWhen 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 skipped
Loading diagram...

CI 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.

yaml
- name: Verify Wally
  run: wally --version

- name: Install Wally packages
  run: wally install

FAQ — 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