Skip to main content
株式会社オブライト
Software Development2026-04-176 min read

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.tomlPackage definitions
testez.ymlTestEZ globals for Selene
DevPackages/Generated by wally install (not in git)
src/ServerScriptService/Tests/Test file directory
Tests/RunTests.server.luauTest runner script
Tests/*.spec.luauIndividual 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

Related free tools (no sign-up, instant results)

Feel free to contact us

Contact Us