roblox-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Testing Roblox games

测试 Roblox 游戏

Make code testable first

首先让代码具备可测试性

  • Keep game rules in pure ModuleScripts (inputs → outputs, no services, no yields): damage formulas, inventory operations, shop pricing, state machines, serialization, validation. These are trivially testable.
  • Services take their dependencies as parameters (dependency injection) so tests can pass fakes instead of real DataStores, remotes, or
    Players
    .
luau
--!strict
-- ReplicatedStorage/Shared/Inventory.luau — pure logic, no Roblox services.
export type Inventory = { [string]: number }

local Inventory = {}

function Inventory.add(inventory: Inventory, itemId: string, amount: number, maxStack: number): (boolean, string?)
	if amount <= 0 or amount ~= math.floor(amount) then
		return false, "invalid amount"
	end
	local current = inventory[itemId] or 0
	if current + amount > maxStack then
		return false, "stack full"
	end
	inventory[itemId] = current + amount
	return true, nil
end

return Inventory
luau
--!strict
-- A service that receives its data store, so tests can inject an in-memory fake.
type Store = {
	GetAsync: (self: Store, key: string) -> any,
	SetAsync: (self: Store, key: string, value: any) -> (),
}

local function newCurrencyService(store: Store)
	local service = {}
	function service.award(userId: number, amount: number): number
		local key = `User_{userId}`
		local balance = (store:GetAsync(key) or 0) + amount
		store:SetAsync(key, balance)
		return balance
	end
	return service
end

-- In tests: a fake store backed by a table.
local fakeData: { [string]: any } = {}
local fakeStore: Store = {
	GetAsync = function(_self, key)
		return fakeData[key]
	end,
	SetAsync = function(_self, key, value)
		fakeData[key] = value
	end,
}
print(newCurrencyService(fakeStore).award(1, 50))

return newCurrencyService
  • 将游戏规则置于 纯 ModuleScripts 中(输入 → 输出,不使用 services、不产生 yields):伤害公式、 库存操作、商店定价、状态机、序列化、验证。这类代码可以轻松测试。
  • Services 将其依赖项作为参数传入(dependency injection),以便测试传入 fake,而不是真实的 DataStores、remotes 或
    Players
    。
luau
--!strict
-- ReplicatedStorage/Shared/Inventory.luau — pure logic, no Roblox services.
export type Inventory = { [string]: number }

local Inventory = {}

function Inventory.add(inventory: Inventory, itemId: string, amount: number, maxStack: number): (boolean, string?)
	if amount <= 0 or amount ~= math.floor(amount) then
		return false, "invalid amount"
	end
	local current = inventory[itemId] or 0
	if current + amount > maxStack then
		return false, "stack full"
	end
	inventory[itemId] = current + amount
	return true, nil
end

return Inventory
luau
--!strict
-- A service that receives its data store, so tests can inject an in-memory fake.
type Store = {
	GetAsync: (self: Store, key: string) -> any,
	SetAsync: (self: Store, key: string, value: any) -> (),
}

local function newCurrencyService(store: Store)
	local service = {}
	function service.award(userId: number, amount: number): number
		local key = `User_{userId}`
		local balance = (store:GetAsync(key) or 0) + amount
		store:SetAsync(key, balance)
		return balance
	end
	return service
end

-- In tests: a fake store backed by a table.
local fakeData: { [string]: any } = {}
local fakeStore: Store = {
	GetAsync = function(_self, key)
		return fakeData[key]
	end,
	SetAsync = function(_self, key, value)
		fakeData[key] = value
	end,
}
print(newCurrencyService(fakeStore).award(1, 50))

return newCurrencyService

Jest Lua (recommended framework)

Jest Lua(推荐框架)

Port of Jest (
describe
/
it
/
expect
, mocks, snapshots). TestEZ is legacy; Jest Lua 3 includes a migration guide.
toml
undefined
Jest 的移植版(
describe
/
it
/
expect
、mocks、snapshots)。TestEZ 是旧版方案;Jest Lua 3 提供了 migration guide。
toml
undefined

wally.toml

wally.toml

[dev-dependencies] Jest = "jsdotlua/jest@3.10.0" JestGlobals = "jsdotlua/jest-globals@3.10.0"

```luau
-- src/shared/Inventory.spec.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local JestGlobals = require(ReplicatedStorage.DevPackages.JestGlobals)
local Inventory = require(ReplicatedStorage.Shared.Inventory)

local describe, it, expect = JestGlobals.describe, JestGlobals.it, JestGlobals.expect

describe("Inventory.add", function()
	it("stacks items up to the limit", function()
		local inventory = {}
		expect(Inventory.add(inventory, "Apple", 3, 5)).toBe(true)
		expect(inventory.Apple).toBe(3)
		local ok, reason = Inventory.add(inventory, "Apple", 3, 5)
		expect(ok).toBe(false)
		expect(reason).toBe("stack full")
	end)

	it("rejects fractional amounts", function()
		expect(Inventory.add({}, "Apple", 0.5, 5)).toBe(false)
	end)
end)
  • Put a
    jest.config.luau
    (
    return { testMatch = { "**/*.spec" } }
    ) in each test root.
  • Runner script (Studio command bar, a test place Script, or cloud task):
    require(ReplicatedStorage.DevPackages.Jest).runCLI(root, { ci = true }, { root }):awaitStatus()
    → inspect
    result.results.numFailedTests
    .
  • Jest Lua 3 needs the fast flag
    FFlagEnableLoadModule = true
    in Studio's
    ClientAppSettings.json
    when run locally.
  • Map
    DevPackages
    into the DataModel only for test builds (separate Rojo project, e.g.
    test.project.json
    ).
[dev-dependencies] Jest = "jsdotlua/jest@3.10.0" JestGlobals = "jsdotlua/jest-globals@3.10.0"

```luau
-- src/shared/Inventory.spec.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local JestGlobals = require(ReplicatedStorage.DevPackages.JestGlobals)
local Inventory = require(ReplicatedStorage.Shared.Inventory)

local describe, it, expect = JestGlobals.describe, JestGlobals.it, JestGlobals.expect

describe("Inventory.add", function()
	it("stacks items up to the limit", function()
		local inventory = {}
		expect(Inventory.add(inventory, "Apple", 3, 5)).toBe(true)
		expect(inventory.Apple).toBe(3)
		local ok, reason = Inventory.add(inventory, "Apple", 3, 5)
		expect(ok).toBe(false)
		expect(reason).toBe("stack full")
	end)

	it("rejects fractional amounts", function()
		expect(Inventory.add({}, "Apple", 0.5, 5)).toBe(false)
	end)
end)
  • 在每个测试根目录中放置一个
    jest.config.luau
    (
    return { testMatch = { "**/*.spec" } }
    )。
  • 运行器脚本(Studio command bar、测试 place 中的 Script 或 cloud task):
    require(ReplicatedStorage.DevPackages.Jest).runCLI(root, { ci = true }, { root }):awaitStatus()
    → 检查
    result.results.numFailedTests
    。
  • 在 Studio 本地运行时,Jest Lua 3 需要在 Studio 的
    ClientAppSettings.json
    中启用 fast flag
    FFlagEnableLoadModule = true
    。
  • 仅为测试构建将
    DevPackages
    映射到 DataModel(使用独立的 Rojo 项目,例如
    test.project.json
    )。

Where tests run

测试运行位置

RunnerUse
Studio (command bar / test Script)Local development.
Studio MCP (
execute_luau
,
start_stop_play
,
get_console_output
)
Agents running tests and playtests against the open place — see
roblox-studio-mcp
.
run-in-roblox
CLI that opens Studio, runs a script, pipes output (needs Studio installed; local/Windows/mac CI runners).
Open Cloud Luau ExecutionHeadless CI: runs a script in a real Roblox server for a published place version.
LunePure-Luau modules with no Roblox API dependencies (fast, runs anywhere).
Open Cloud Luau Execution flow (details and a script: references/cloud-test-runner.md):
  1. Build the test place (
    rojo build test.project.json -o test.rbxl
    ) and upload it as a Saved version of a dedicated test place.
  2. POST /cloud/v2/universes/{u}/places/{p}/versions/{v}/luau-execution-session-tasks
    with
    { "script": ... }
    (API key scope
    universe.place.luau-execution-session:write
    ; rate limit ≈ 5 tasks/min per key owner).
  3. Poll the task
    path
    until
    state
    is
    COMPLETE
    /
    FAILED
    ; read
    output.results
    and
    .../logs
    .
运行器用途
Studio(command bar / 测试 Script)本地开发。
Studio MCP(
execute_luau
、
start_stop_play
、
get_console_output
)
供 Agent 针对打开的 place 运行测试和试玩——参见
roblox-studio-mcp
。
run-in-roblox
通过 CLI 打开 Studio、运行脚本并传递输出(需要安装 Studio;适用于本地/Windows/mac CI runners)。
Open Cloud Luau Execution无头 CI:针对已发布的 place 版本,在真实 Roblox server 中运行脚本。
Lune不依赖 Roblox API 的纯 Luau 模块(速度快,可随处运行)。
Open Cloud Luau Execution 流程(详情和脚本:references/cloud-test-runner.md):
  1. 构建测试 place(
    rojo build test.project.json -o test.rbxl
    ),并将其作为专用测试 place 的 Saved 版本上传。
  2. POST /cloud/v2/universes/{u}/places/{p}/versions/{v}/luau-execution-session-tasks
    ,请求体为
    { "script": ... }
    (API key 权限范围
    universe.place.luau-execution-session:write
    ;每个 key 所有者限速 ≈ 5 个任务/分钟)。
  3. 轮询任务
    path
    ,直至
    state
    为
    COMPLETE
    /
    FAILED
    ;读取
    output.results
    和
    .../logs
    。

Playtesting in Studio

在 Studio 中进行试玩

  • Test tab: Play (you + server), Run (server only, no character), Clients and Servers with 2+ players to test replication, trading, and PvP. Most multiplayer bugs only appear with ≥ 2 clients.
  • Network simulation (incoming latency, jitter, packet loss) to test lag compensation.
  • Device Emulator (phones, tablets, consoles) and Controller Emulator for input/UI.
  • Streaming: test with
    StreamingTargetRadius = 64
    .
  • DataStores in Studio: enable API access and use a separate test universe or data store names.
  • Test 标签页:Play(你 + server)、Run(仅 server、无角色),以及使用 2 个以上玩家的 Clients and Servers,用于测试 replication、trading 和 PvP。大多数多人游戏 bug 只会在 ≥ 2 个 clients 时出现。
  • 网络模拟(入站延迟、抖动、丢包),用于测试延迟补偿。
  • Device Emulator(手机、平板、主机)和 Controller Emulator,用于测试输入/UI。
  • Streaming:使用
    StreamingTargetRadius = 64
    进行测试。
  • Studio 中的 DataStores:启用 API access,并使用独立的测试 universe 或 data store 名称。

QA checklist before shipping a feature

发布功能前的 QA 清单

  • Works for a player who joins after the feature started (late joiners), and after respawn.
  • Works with 2+ clients; server state is authoritative; UI updates on all clients.
  • Player leaving mid-action (trade, purchase, round) leaves no broken state or leaked tables.
  • Remotes rejected when spammed or sent malformed arguments.
  • Data saves and reloads (rejoin), including teleports between places.
  • Mobile, gamepad, and keyboard all work; UI readable on a phone.
  • No errors/warnings in Output (server and client).
  • 对在功能启动后加入的玩家(late joiners)以及 respawn 后均有效。
  • 在 2 个以上 clients 下有效;server state 保持 authoritative;UI 在所有 clients 上更新。
  • 玩家在操作中途离开(trade、purchase、round)后,不会留下损坏状态或泄漏 tables。
  • Remotes 在被 spam 或收到 malformed arguments 时会被拒绝。
  • 数据可保存并重新加载(重新加入),包括在 places 之间 teleport。
  • Mobile、gamepad 和 keyboard 均可使用;UI 在手机上可读。
  • Output(server 和 client)中没有 errors/warnings。

Related skills

相关 skills

roblox-tooling
(CI, Rojo),
roblox-studio-mcp
(agent-driven playtests),
roblox-open-cloud
,
roblox-code-review
.
roblox-tooling
(CI、Rojo)、
roblox-studio-mcp
(Agent 驱动的试玩)、
roblox-open-cloud
、
roblox-code-review
。