roblox-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chineseroblox-testing
roblox-testing
Official sources:
- https://create.roblox.com/docs/en-us/studio/developer-console
- https://create.roblox.com/docs/en-us/performance-optimization/identify
- https://create.roblox.com/docs/en-us/performance-optimization/microprofiler
- https://create.roblox.com/docs/en-us/performance-optimization/scene-analysis
- https://create.roblox.com/docs/en-us/studio/optimization/memory-usage
- https://create.roblox.com/docs/en-us/studio/optimization/scriptprofiler
Start with a reproducible failure and identify whether it occurs on the client, server, or both. Choose a debugging or profiling tool based on that evidence.
官方资料:
- https://create.roblox.com/docs/en-us/studio/developer-console
- https://create.roblox.com/docs/en-us/performance-optimization/identify
- https://create.roblox.com/docs/en-us/performance-optimization/microprofiler
- https://create.roblox.com/docs/en-us/performance-optimization/scene-analysis
- https://create.roblox.com/docs/en-us/studio/optimization/memory-usage
- https://create.roblox.com/docs/en-us/studio/optimization/scriptprofiler
先从可复现的故障入手,确定问题发生在客户端、服务器还是两端。然后根据这些证据选择合适的调试或性能分析工具。
The debugging mindset
调试思路
- Reproduce it. If you can't reproduce it, you can't fix it.
- Isolate it. Remove systems until the bug disappears; the last thing removed is the cause.
- Measure it. Use tools instead of guessing.
- Fix one thing at a time. Verify the fix and add a regression test if possible.
- Log defensively. Good logs make future debugging faster.
- 复现问题。 如果无法复现,就无法修复。
- 隔离问题。 逐一移除系统,直到 bug 消失;最后移除的系统就是根因。
- 进行测量。 使用工具,不要靠猜测。
- 每次只修复一个问题。 验证修复,并尽可能添加回归测试。
- 妥善记录日志。 好的日志能让后续调试更快捷。
Logging discipline
日志规范
Use , , and deliberately:
printwarnerror- for normal diagnostics.
print - for recoverable problems you should notice.
warn - for programming errors that should stop execution.
error
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.
有意识地使用 、 和 :
printwarnerror- 用于常规诊断。
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)
endUse for internal invariants that should never fail:
assertlua
assert(config.MaxSpeed > 0, "MaxSpeed must be positive")使用 包裹可能失败的调用,尤其是云服务、HTTP、DataStores 和 Marketplace:
pcalllua
local ok, result = pcall(function()
return someService:DoSomething()
end)
if not ok then
warn("DoSomething failed:", result)
end对于绝不应失败的内部不变量,使用 :
assertlua
assert(config.MaxSpeed > 0, "MaxSpeed must be positive")Developer Console
Developer Console
Open with in-game or in Studio play mode.
F9Tabs:
- 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 / blocks, lifecycle hooks, async tests, and a rich matcher API.
describeitTestEZ 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, can mark failures visibly in Studio output, but most projects should prefer TestEZ. The scripts folder contains a minimal shim only for environments where TestEZ is unavailable.
TestService:Error(msg)TestRunner.luaTestEZ 示例:
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 内测试,可以使用 在 Studio 输出中醒目标记失败,但大多数项目应优先使用 TestEZ。scripts 文件夹包含一个精简的 适配层(shim),仅供无法使用 TestEZ 的环境使用。
TestService:Error(msg)TestRunner.luaLuau type checking
Luau 类型检查
Use the Luau language server () or the Luau type checker in Studio to catch errors before runtime.
luau-lspBest practices:
- Add type annotations to module exports and pure functions.
- Run in CI or before committing.
luau-lsp analyze - Enable for new modules.
--!strict - Do not over-type untyped engine APIs; prefer casts at boundaries.
使用 Luau language server()或 Studio 中的 Luau type checker,在运行时之前捕获错误。
luau-lsp最佳实践:
- 为模块导出和纯函数添加类型注解。
- 在 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 with to capture full stack traces from failures:
xpcalldebug.tracebacklua
local ok, err = xpcall(function()
riskyOperation()
end, debug.traceback)
if not ok then
warn(err)
endThis is especially useful at top-level entry points, scheduled callbacks, and connection handlers where alone discards the stack.
pcall使用 配合 捕获故障的完整堆栈跟踪:
xpcalldebug.tracebacklua
local ok, err = xpcall(function()
riskyOperation()
end, debug.traceback)
if not ok then
warn(err)
end这尤其适用于顶层入口点、定时回调和连接处理函数,因为单独使用 会丢失堆栈信息。
pcallConnection cleanup
连接清理
Leaked objects are a common source of memory leaks and stale state. Prefer explicit cleanup patterns:
RBXScriptConnection- 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 for one-shot event handlers.
:Once() - Use to trigger cleanup when an instance is removed:
Instance.Destroying
lua
obj.Destroying:Connect(function()
cleanup()
end)- Always pair with a matching
:Connect()or destruction.:Disconnect()
泄漏的 对象是内存泄漏和状态残留的常见来源。优先采用显式清理模式:
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 library is more predictable than legacy , , and :
taskspawndelaywaitlua
-- good
task.wait(1)
task.delay(1, callback)
task.spawn(coroutineOrFn)
-- avoid
spawn(callback)
delay(1, callback)
wait(1)task.defertaskspawndelaywaitlua
-- good
task.wait(1)
task.delay(1, callback)
task.spawn(coroutineOrFn)
-- avoid
spawn(callback)
delay(1, callback)
wait(1)task.deferLog 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
endNetwork 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 DebugDrawUse to inspect ownership. Vehicles and held items should usually be owned by the controlling player.
BasePart:GetNetworkOwner()网络所有权错误会导致物理抖动和复制延迟。可视化网络所有权并显式设置:
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
endAlways 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:
- Capture a baseline snapshot in a stable state.
- Play through the action that may leak (spawn/despawn enemies, open/close UI).
- Return to the stable state and capture a second snapshot.
- Compare PlaceMemory, Luau heap, and instance counts.
- Investigate categories that did not return to baseline; look for unparented instances in Scene Analysis.
使用 Developer Console 的 Memory 标签页或 Scene Analysis 比较快照:
- 在稳定状态下捕获基线快照。
- 完整执行可能引发内存泄漏的操作(生成/销毁敌人、打开/关闭 UI)。
- 回到稳定状态并捕获第二个快照。
- 比较 PlaceMemory、Luau heap 和实例数量。
- 调查哪些类别未恢复至基线;在 Scene Analysis 中查找未挂载的实例。
MicroProfiler
MicroProfiler
Open with () in Studio or the desktop client.
Ctrl+F6⌘+F6Use 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 API.
SceneAnalysisService在 Studio 中可通过 Window → Performance Summary → Scene Analysis 使用。
视图:
- 脚本内存:各个脚本的 Luau heap。
- 未挂载实例:脚本持有的潜在内存泄漏。
- 实例构成:按类别统计数量。
- 音频/动画内存:资源内存使用情况。
- 三角形构成:draw call 细分情况。
Scene Analysis 是 Studio UI 工具;没有公开的 API。
SceneAnalysisServiceScript 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 类别
| Symptom | Likely causes |
|---|---|
| Script silently fails | Missing |
| Data not saving | DataStore called from client, non-serializable value, no |
| Remote not working | Wrong side, handler not connected, argument mismatch |
| Lag spikes | Pathfinding every frame, too many particles, unbatched loops |
| Physics jitter | Wrong network ownership, assembly splits, conflicting constraints |
| NPCs stuck | Blocked path not recomputed, bad agent params, streaming issues |
| UI doesn't update | Property not replicated, wrong parent, layout order |
| Memory grows forever | Leaked connections, unparented instances, cached assets |
| 症状 | 可能原因 |
|---|---|
| 脚本静默失败 | 缺少 |
| 数据未保存 | 从客户端调用 DataStore、值无法序列化、缺少 |
| Remote 无法工作 | 端点错误、处理函数未连接、参数不匹配 |
| 卡顿尖峰 | 每帧寻路、粒子过多、未批处理的循环 |
| 物理抖动 | 网络所有权错误、assembly 拆分、约束冲突 |
| NPC 卡住 | 阻塞路径未重新计算、agent 参数错误、流式加载问题 |
| UI 不更新 | 属性未复制、父级错误、布局顺序错误 |
| 内存持续增长 | 连接泄漏、未挂载实例、缓存资源 |
Network debugging
网络调试
- Check Network tab in Developer Console for HTTP/DataStore failures.
- Use in-game for network debug stats.
Shift+F3 - 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
- : a minimal TestEZ fallback with nested suites, lifecycle hooks, matchers, async support, and TestService integration.
scripts/TestRunner.lua - : a structured logger with level filtering and guarded formatting.
scripts/Logger.lua - : utility for drawing rays, points, and boxes in 3D for visual debugging.
scripts/DebugDraw.lua
- :一个精简的 TestEZ 回退实现,支持嵌套测试套件、生命周期钩子、matcher、异步支持和 TestService 集成。
scripts/TestRunner.lua - :带有级别过滤和安全格式处理机制的结构化 logger。
scripts/Logger.lua - :用于在 3D 中绘制射线、点和盒体,以进行可视化调试的实用工具。
scripts/DebugDraw.lua
How to proceed
操作步骤
- Reproduce the issue reliably.
- Check logs and errors in the Developer Console.
- Determine client vs server scope.
- Use MicroProfiler/Script Profiler for performance issues.
- Use Scene Analysis for memory leaks and scene composition.
- Add targeted logging or tests to confirm the fix.
- Verify on low-end devices and with network simulation when relevant.
- Compare memory snapshots before and after suspected leaks.
- 可靠地复现问题。
- 检查 Developer Console 中的日志和错误。
- 确定问题范围是客户端还是服务器。
- 对性能问题使用 MicroProfiler/Script Profiler。
- 对内存泄漏和场景构成问题使用 Scene Analysis。
- 添加有针对性的日志或测试,以确认修复。
- 相关时,在低端设备和网络模拟环境下进行验证。
- 对比疑似泄漏发生前后的内存快照。
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.
- common-bugs-and-fixes.md:根据症状追踪可能原因,并制定针对性修复方案。
- debugging-tools.md:检查日志、断点、堆栈跟踪或运行时状态。
- network-simulation.md:在 Studio 中重现延迟、jitter 或丢包问题。
- performance-profiling.md:查找 CPU、GPU、内存或加载时间瓶颈。
- testing-patterns.md:编写单元测试、mock 依赖项,或测试客户端—服务器流程。