roblox-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

roblox-testing

roblox-testing

The debugging mindset

调试思路

  1. Reproduce it. If you can't reproduce it, you can't fix it.
  2. Isolate it. Remove systems until the bug disappears; the last thing removed is the cause.
  3. Measure it. Use tools instead of guessing.
  4. Fix one thing at a time. Verify the fix and add a regression test if possible.
  5. Log defensively. Good logs make future debugging faster.
  1. 复现问题。 如果无法复现,就无法修复。
  2. 隔离问题。 逐一移除系统,直到 bug 消失;最后移除的系统就是根因。
  3. 进行测量。 使用工具,不要靠猜测。
  4. 每次只修复一个问题。 验证修复,并尽可能添加回归测试。
  5. 妥善记录日志。 好的日志能让后续调试更快捷。

Logging discipline

日志规范

Use
print
,
warn
, and
error
deliberately:
  • print
    for normal diagnostics.
  • warn
    for recoverable problems you should notice.
  • error
    for programming errors that should stop execution.
Include context in log messages:
lua
warn(string.format("[DataStore] Save failed for %d: %s", userId, tostring(err)))
Avoid logging secrets, player data, or PII.
有意识地使用
print
、
warn
和
error
:
  • print
    用于常规诊断。
  • warn
    用于值得注意且可恢复的问题。
  • error
    用于应当中止执行的编程错误。
在日志消息中包含上下文:
lua
warn(string.format("[DataStore] Save failed for %d: %s", userId, tostring(err)))
避免记录机密信息、玩家数据或 PII。

pcall and assertions

pcall 与断言

Wrap fallible calls, especially cloud services, HTTP, DataStores, Marketplace:
lua
local ok, result = pcall(function()
    return someService:DoSomething()
end)
if not ok then
    warn("DoSomething failed:", result)
end
Use
assert
for internal invariants that should never fail:
lua
assert(config.MaxSpeed > 0, "MaxSpeed must be positive")
使用
pcall
包裹可能失败的调用,尤其是云服务、HTTP、DataStores 和 Marketplace:
lua
local ok, result = pcall(function()
    return someService:DoSomething()
end)
if not ok then
    warn("DoSomething failed:", result)
end
对于绝不应失败的内部不变量,使用
assert
:
lua
assert(config.MaxSpeed > 0, "MaxSpeed must be positive")

Developer Console

Developer Console

Open with
F9
in-game or in Studio play mode.
Tabs:
  • Log: client/server output, errors, warnings.
  • Memory: categorized memory usage.
  • Network: HTTP and service requests.
  • Server Stats: heartbeat, ping, data ping.
  • Script Profiler: record script CPU usage.
  • MicroProfiler: capture server dumps.
Toggle Client/Server views to see which side emitted output.
在游戏内或 Studio 播放模式下按
F9
打开。
标签页:
  • Log:客户端/服务器输出、错误和警告。
  • Memory:按类别划分的内存使用量。
  • Network:HTTP 和服务请求。
  • Server Stats:heartbeat、ping、data ping。
  • Script Profiler:记录脚本 CPU 使用情况。
  • MicroProfiler:捕获服务器 dump。
切换 Client/Server 视图,以确定输出来自哪一端。

Unit testing with TestEZ

使用 TestEZ 进行单元测试

The standard Roblox testing framework is TestEZ. It supports nested
describe
/
it
blocks, lifecycle hooks, async tests, and a rich matcher API.
TestEZ example:
lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage.DevPackages.TestEZ)

local MathUtils = require(ReplicatedStorage.MathUtils)

describe("MathUtils", function()
    it("clamps values", function()
        expect(MathUtils.clamp(5, 0, 10)).to.equal(5)
        expect(MathUtils.clamp(-1, 0, 10)).to.equal(0)
        expect(MathUtils.clamp(11, 0, 10)).to.equal(10)
    end)
end)

TestEZ.TestBootstrap:run({ game.ReplicatedStorage.Tests })
For legacy in-Studio tests,
TestService:Error(msg)
can mark failures visibly in Studio output, but most projects should prefer TestEZ. The scripts folder contains a minimal
TestRunner.lua
shim only for environments where TestEZ is unavailable.
Roblox 的标准测试框架是 TestEZ。它支持嵌套的
describe
/
it
块、生命周期钩子、异步测试以及丰富的 matcher API。
TestEZ 示例:
lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage.DevPackages.TestEZ)

local MathUtils = require(ReplicatedStorage.MathUtils)

describe("MathUtils", function()
    it("clamps values", function()
        expect(MathUtils.clamp(5, 0, 10)).to.equal(5)
        expect(MathUtils.clamp(-1, 0, 10)).to.equal(0)
        expect(MathUtils.clamp(11, 0, 10)).to.equal(10)
    end)
end)

TestEZ.TestBootstrap:run({ game.ReplicatedStorage.Tests })
对于旧版 Studio 内测试,可以使用
TestService:Error(msg)
在 Studio 输出中醒目标记失败,但大多数项目应优先使用 TestEZ。scripts 文件夹包含一个精简的
TestRunner.lua
适配层(shim),仅供无法使用 TestEZ 的环境使用。

Luau type checking

Luau 类型检查

Use the Luau language server (
luau-lsp
) or the Luau type checker in Studio to catch errors before runtime.
Best practices:
  • Add type annotations to module exports and pure functions.
  • Run
    luau-lsp analyze
    in CI or before committing.
  • Enable
    --!strict
    for new modules.
  • Do not over-type untyped engine APIs; prefer casts at boundaries.
使用 Luau language server(
luau-lsp
)或 Studio 中的 Luau type checker,在运行时之前捕获错误。
最佳实践:
  • 为模块导出和纯函数添加类型注解。
  • 在 CI 中或提交前运行
    luau-lsp analyze
    。
  • 为新模块启用
    --!strict
    。
  • 不要为未类型化的引擎 API 过度添加类型;更推荐在边界处使用 cast。

Studio Debugger

Studio Debugger

Use Studio's built-in debugger for step-through debugging:
  • Set breakpoints by clicking the gutter next to a line number.
  • Run in Play mode and use the debugger controls to step over, into, or out.
  • Inspect the call stack, local variables, and upvalues in the debugger panel.
  • The debugger works in both client and server contexts when Studio runs both.
使用 Studio 内置 debugger 进行单步调试:
  • 点击行号旁的代码装订线设置断点。
  • 在 Play 模式下运行,并使用调试器控件执行 step over、step into 或 step out。
  • 在调试器面板中检查调用栈、局部变量和 upvalues。
  • 当 Studio 同时运行客户端和服务器时,调试器在两种上下文中都可用。

xpcall and debug.traceback

xpcall 与 debug.traceback

Use
xpcall
with
debug.traceback
to capture full stack traces from failures:
lua
local ok, err = xpcall(function()
    riskyOperation()
end, debug.traceback)
if not ok then
    warn(err)
end
This is especially useful at top-level entry points, scheduled callbacks, and connection handlers where
pcall
alone discards the stack.
使用
xpcall
配合
debug.traceback
捕获故障的完整堆栈跟踪:
lua
local ok, err = xpcall(function()
    riskyOperation()
end, debug.traceback)
if not ok then
    warn(err)
end
这尤其适用于顶层入口点、定时回调和连接处理函数,因为单独使用
pcall
会丢失堆栈信息。

Connection cleanup

连接清理

Leaked
RBXScriptConnection
objects are a common source of memory leaks and stale state. Prefer explicit cleanup patterns:
  • Use a Maid/Janitor-style collector:
lua
local Maid = require(path.to.Maid)
local maid = Maid.new()

maid:GiveTask(workspace.ChildAdded:Connect(onChild))
maid:GiveTask(RunService.Heartbeat:Connect(onStep))

maid:Destroy() -- disconnects everything
  • Use
    :Once()
    for one-shot event handlers.
  • Use
    Instance.Destroying
    to trigger cleanup when an instance is removed:
lua
obj.Destroying:Connect(function()
    cleanup()
end)
  • Always pair
    :Connect()
    with a matching
    :Disconnect()
    or destruction.
泄漏的
RBXScriptConnection
对象是内存泄漏和状态残留的常见来源。优先采用显式清理模式:
  • 使用 Maid/Janitor 风格的收集器:
lua
local Maid = require(path.to.Maid)
local maid = Maid.new()

maid:GiveTask(workspace.ChildAdded:Connect(onChild))
maid:GiveTask(RunService.Heartbeat:Connect(onStep))

maid:Destroy() -- disconnects everything
  • 对一次性事件处理函数使用
    :Once()
    。
  • 使用
    Instance.Destroying
    ,在实例被移除时触发清理:
lua
obj.Destroying:Connect(function()
    cleanup()
end)
  • 始终让每个
    :Connect()
    都与相应的
    :Disconnect()
    或销毁操作配对。

Prefer task over spawn/delay

优先使用 task,而非 spawn/delay

The
task
library is more predictable than legacy
spawn
,
delay
, and
wait
:
lua
-- good
task.wait(1)
task.delay(1, callback)
task.spawn(coroutineOrFn)

-- avoid
spawn(callback)
delay(1, callback)
wait(1)
task.defer
is useful for yielding to the next resumption cycle without opening a new thread.
task
库比旧版
spawn
、
delay
和
wait
更可预测:
lua
-- good
task.wait(1)
task.delay(1, callback)
task.spawn(coroutineOrFn)

-- avoid
spawn(callback)
delay(1, callback)
wait(1)
task.defer
适合在下一个恢复周期让出执行权,而无需创建新线程。

Log rate limiting and secrets hygiene

日志限流与机密信息处理

  • Never log secrets, keys, user identifiers, or personal information.
  • Sanitize values before logging:
lua
local function sanitize(value)
    if typeof(value) == "Instance" and value:IsA("Player") then
        return "Player(" .. tostring(value.UserId) .. ")"
    end
    return value
end
  • Rate-limit noisy logs to avoid flooding the console and network:
lua
local lastLog = 0
local function throttledLog(message)
    local now = os.clock()
    if now - lastLog > 5 then
        lastLog = now
        warn(message)
    end
end
  • 绝不要记录机密信息、密钥、用户标识符或个人信息。
  • 在记录日志前对值进行脱敏:
lua
local function sanitize(value)
    if typeof(value) == "Instance" and value:IsA("Player") then
        return "Player(" .. tostring(value.UserId) .. ")"
    end
    return value
end
  • 对高频日志进行限流,避免控制台和网络被大量日志淹没:
lua
local lastLog = 0
local function throttledLog(message)
    local now = os.clock()
    if now - lastLog > 5 then
        lastLog = now
        warn(message)
    end
end

Network ownership

网络所有权

Incorrect network ownership causes jittery physics and replication lag. Visualize ownership and assign it explicitly:
lua
-- Server
part:SetNetworkOwner(player)

-- Client-side visualization (debug only)
local ownershipLabel -- create BillboardGui or use DebugDraw
Use
BasePart:GetNetworkOwner()
to inspect ownership. Vehicles and held items should usually be owned by the controlling player.
网络所有权错误会导致物理抖动和复制延迟。可视化网络所有权并显式设置:
lua
-- Server
part:SetNetworkOwner(player)

-- Client-side visualization (debug only)
local ownershipLabel -- create BillboardGui or use DebugDraw
使用
BasePart:GetNetworkOwner()
检查网络所有权。载具和手持物品的网络所有权通常应归控制它们的玩家。

DataStore retry pattern

DataStore 重试模式

For retry-safe reads, use bounded retries with exponential backoff and jitter. Do not use this generic loop to replay writes: a failed response can hide a committed write, and independently retried requests can execute out of order. See roblox-datastores for write reconciliation and per-key ordering.
lua
local function readWithRetry(fn, maxAttempts)
    maxAttempts = maxAttempts or 5
    for attempt = 1, maxAttempts do
        local ok, result = pcall(fn)
        if ok then
            return true, result
        elseif attempt == maxAttempts then
            return false, result
        else
            warn("DataStore attempt " .. attempt .. " failed; retrying...")
            task.wait(2 ^ attempt * 0.1 + math.random() * 0.5)
        end
    end
end
Always call DataStores from the server and validate serialization before saving.
对于可安全重试的读取操作,应使用有限次数的重试,并结合指数退避和 jitter(抖动)。不要使用这个通用循环重放写操作:失败响应可能掩盖已经提交的写入,而彼此独立重试的请求可能乱序执行。有关写入协调和按 key 排序,请参阅 roblox-datastores。
lua
local function readWithRetry(fn, maxAttempts)
    maxAttempts = maxAttempts or 5
    for attempt = 1, maxAttempts do
        local ok, result = pcall(fn)
        if ok then
            return true, result
        elseif attempt == maxAttempts then
            return false, result
        else
            warn("DataStore attempt " .. attempt .. " failed; retrying...")
            task.wait(2 ^ attempt * 0.1 + math.random() * 0.5)
        end
    end
end
始终从服务器调用 DataStores,并在保存前验证序列化结果。

Memory snapshot comparison

内存快照对比

Use the Developer Console Memory tab or Scene Analysis to compare snapshots:
  1. Capture a baseline snapshot in a stable state.
  2. Play through the action that may leak (spawn/despawn enemies, open/close UI).
  3. Return to the stable state and capture a second snapshot.
  4. Compare PlaceMemory, Luau heap, and instance counts.
  5. Investigate categories that did not return to baseline; look for unparented instances in Scene Analysis.
使用 Developer Console 的 Memory 标签页或 Scene Analysis 比较快照:
  1. 在稳定状态下捕获基线快照。
  2. 完整执行可能引发内存泄漏的操作(生成/销毁敌人、打开/关闭 UI)。
  3. 回到稳定状态并捕获第二个快照。
  4. 比较 PlaceMemory、Luau heap 和实例数量。
  5. 调查哪些类别未恢复至基线;在 Scene Analysis 中查找未挂载的实例。

MicroProfiler

MicroProfiler

Open with
Ctrl+F6
(
⌘+F6
) in Studio or the desktop client.
Use it to:
  • Find frame-time spikes.
  • Identify whether a bottleneck is script compute, physics, or rendering.
  • Capture server dumps from the Developer Console.
  • Add custom labels with
    debug.profilebegin
    /
    debug.profileend
    .
Key colors:
  • Orange: worker thread (scripts, physics, animations) bottleneck.
  • Blue: render thread bottleneck.
  • Red: GPU wait / render complexity.
在 Studio 或桌面客户端中按
Ctrl+F6
(
⌘+F6
)打开。
用途:
  • 查找帧时间峰值。
  • 确定瓶颈来自脚本计算、物理还是渲染。
  • 从 Developer Console 捕获服务器 dump。
  • 使用
    debug.profilebegin
    /
    debug.profileend
    添加自定义标签。
关键颜色:
  • 橙色:工作线程(脚本、物理、动画)成为瓶颈。
  • 蓝色:渲染线程成为瓶颈。
  • 红色:GPU 等待/渲染复杂度。

Scene Analysis

Scene Analysis

Available in Studio under Window → Performance Summary → Scene Analysis.
Views:
  • Script memory: per-script Luau heap.
  • Unparented instances: potential memory leaks held by scripts.
  • Instance composition: counts by category.
  • Audio/Animation memory: asset memory usage.
  • Triangle composition: draw call breakdown.
Scene Analysis is a Studio UI tool; there is no public
SceneAnalysisService
API.
在 Studio 中可通过 Window → Performance Summary → Scene Analysis 使用。
视图:
  • 脚本内存:各个脚本的 Luau heap。
  • 未挂载实例:脚本持有的潜在内存泄漏。
  • 实例构成:按类别统计数量。
  • 音频/动画内存:资源内存使用情况。
  • 三角形构成:draw call 细分情况。
Scene Analysis 是 Studio UI 工具;没有公开的
SceneAnalysisService
API。

Script Profiler

Script Profiler

Records CPU time per script. Use it when MicroProfiler points to scripts but you need to know which script.
记录每个脚本的 CPU 时间。当 MicroProfiler 指向脚本、但需要确定具体是哪个脚本时使用它。

Common bug categories

常见 Bug 类别

SymptomLikely causes
Script silently failsMissing
pcall
, error swallowed, wrong script context
Data not savingDataStore called from client, non-serializable value, no
pcall
Remote not workingWrong side, handler not connected, argument mismatch
Lag spikesPathfinding every frame, too many particles, unbatched loops
Physics jitterWrong network ownership, assembly splits, conflicting constraints
NPCs stuckBlocked path not recomputed, bad agent params, streaming issues
UI doesn't updateProperty not replicated, wrong parent, layout order
Memory grows foreverLeaked connections, unparented instances, cached assets
症状可能原因
脚本静默失败缺少
pcall
、错误被吞掉、脚本上下文错误
数据未保存从客户端调用 DataStore、值无法序列化、缺少
pcall
Remote 无法工作端点错误、处理函数未连接、参数不匹配
卡顿尖峰每帧寻路、粒子过多、未批处理的循环
物理抖动网络所有权错误、assembly 拆分、约束冲突
NPC 卡住阻塞路径未重新计算、agent 参数错误、流式加载问题
UI 不更新属性未复制、父级错误、布局顺序错误
内存持续增长连接泄漏、未挂载实例、缓存资源

Network debugging

网络调试

  • Check Network tab in Developer Console for HTTP/DataStore failures.
  • Use
    Shift+F3
    in-game for network debug stats.
  • Distinguish network ping (round-trip time) from data ping (replication queue).
  • Use Studio's beta Network Simulator for separate inbound/outbound latency, jitter, and packet loss. Enable New Device Simulator in Beta Features, restart Studio, then open Test → Device Simulator → Network. Select Apply to activate staged changes. Ideal Fiber still adds 8 ms each way; use all-zero custom values for a zero-added-delay baseline. See network simulation.
  • 检查 Developer Console 的 Network 标签页,查找 HTTP/DataStore 故障。
  • 在游戏内按
    Shift+F3
    查看网络调试统计信息。
  • 区分网络 ping(往返时间)和 data ping(复制队列)。
  • 使用 Studio 的 beta Network Simulator 分别模拟入站/出站延迟、jitter 和丢包。在 Beta Features 中启用 New Device Simulator,重启 Studio,然后打开 Test → Device Simulator → Network。选择 Apply 以激活待生效的更改。Ideal Fiber 仍会在每个方向增加 8 ms;如需额外延迟为零的基线,请使用所有值均为零的自定义设置。请参阅网络模拟。

Load time debugging

加载时间调试

Measure load time:
lua
local start = os.clock()
game.Loaded:Connect(function()
    print("Loaded in", os.clock() - start)
end)
Enable Print Join Size Breakdown in Studio Settings → Network to see the largest replicated instances.
测量加载时间:
lua
local start = os.clock()
game.Loaded:Connect(function()
    print("Loaded in", os.clock() - start)
end)
在 Studio Settings → Network 中启用 Print Join Size Breakdown,查看复制量最大的实例。

Scripts

Scripts

  • scripts/TestRunner.lua
    : a minimal TestEZ fallback with nested suites, lifecycle hooks, matchers, async support, and TestService integration.
  • scripts/Logger.lua
    : a structured logger with level filtering and guarded formatting.
  • scripts/DebugDraw.lua
    : utility for drawing rays, points, and boxes in 3D for visual debugging.
  • scripts/TestRunner.lua
    :一个精简的 TestEZ 回退实现,支持嵌套测试套件、生命周期钩子、matcher、异步支持和 TestService 集成。
  • scripts/Logger.lua
    :带有级别过滤和安全格式处理机制的结构化 logger。
  • scripts/DebugDraw.lua
    :用于在 3D 中绘制射线、点和盒体,以进行可视化调试的实用工具。

How to proceed

操作步骤

  1. Reproduce the issue reliably.
  2. Check logs and errors in the Developer Console.
  3. Determine client vs server scope.
  4. Use MicroProfiler/Script Profiler for performance issues.
  5. Use Scene Analysis for memory leaks and scene composition.
  6. Add targeted logging or tests to confirm the fix.
  7. Verify on low-end devices and with network simulation when relevant.
  8. Compare memory snapshots before and after suspected leaks.
<!-- catalog:references:start -->
  1. 可靠地复现问题。
  2. 检查 Developer Console 中的日志和错误。
  3. 确定问题范围是客户端还是服务器。
  4. 对性能问题使用 MicroProfiler/Script Profiler。
  5. 对内存泄漏和场景构成问题使用 Scene Analysis。
  6. 添加有针对性的日志或测试,以确认修复。
  7. 相关时,在低端设备和网络模拟环境下进行验证。
  8. 对比疑似泄漏发生前后的内存快照。
<!-- catalog:references:start -->

Reference index

参考索引

  • common-bugs-and-fixes.md: Trace a symptom to likely causes and a focused fix.
  • debugging-tools.md: Inspect logs, breakpoints, stack traces, or runtime state.
  • network-simulation.md: Reproduce latency, jitter, or packet-loss problems in Studio.
  • performance-profiling.md: Find CPU, GPU, memory, or load-time bottlenecks.
  • testing-patterns.md: Write unit tests, mock dependencies, or test client-server flows.
<!-- catalog:references:end -->
  • common-bugs-and-fixes.md:根据症状追踪可能原因,并制定针对性修复方案。
  • debugging-tools.md:检查日志、断点、堆栈跟踪或运行时状态。
  • network-simulation.md:在 Studio 中重现延迟、jitter 或丢包问题。
  • performance-profiling.md:查找 CPU、GPU、内存或加载时间瓶颈。
  • testing-patterns.md:编写单元测试、mock 依赖项,或测试客户端—服务器流程。
<!-- catalog:references:end -->