roblox-testing
Test, debug, or profile Roblox experiences. Use for Studio debugging, TestEZ, logging, connection cleanup, memory or network diagnostics, MicroProfiler, Scene Analysis, and Script Profiler.
Install
npx skills add https://github.com/nonlooped/roblox-suite/tree/main/roblox-testing
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install nonlooped-roblox-suite@llmmart
git clone https://github.com/nonlooped/roblox-suite.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole nonlooped/roblox-suite collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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.
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.
Logging discipline
Use print, warn, and error deliberately:
printfor normal diagnostics.warnfor recoverable problems you should notice.errorfor programming errors that should stop execution.
Include context in log messages:
warn(string.format("[DataStore] Save failed for %d: %s", userId, tostring(err)))
Avoid logging secrets, player data, or PII.
pcall and assertions
Wrap fallible calls, especially cloud services, HTTP, DataStores, Marketplace:
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:
assert(config.MaxSpeed > 0, "MaxSpeed must be positive")
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.
Unit testing with 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:
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.
Luau type checking
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 analyzein CI or before committing. - Enable
--!strictfor new modules. - Do not over-type untyped engine APIs; prefer casts at boundaries.
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.
xpcall and debug.traceback
Use xpcall with debug.traceback to capture full stack traces from failures:
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.
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:
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.Destroyingto trigger cleanup when an instance is removed:
obj.Destroying:Connect(function()
cleanup()
end)
- Always pair
:Connect()with a matching:Disconnect()or destruction.
Prefer task over spawn/delay
The task library is more predictable than legacy spawn, delay, and wait:
-- 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.
Log rate limiting and secrets hygiene
- Never log secrets, keys, user identifiers, or personal information.
- Sanitize values before logging:
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:
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:
-- 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.
DataStore retry pattern
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.
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.
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.
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.
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.
Script Profiler
Records CPU time per script. Use it when MicroProfiler points to scripts but you need to know which script.
Common bug categories
| Symptom | Likely causes |
|---|---|
| Script silently fails | Missing pcall, error swallowed, wrong script context |
| Data not saving | DataStore called from client, non-serializable value, no pcall |
| 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 |
Network debugging
- Check Network tab in Developer Console for HTTP/DataStore failures.
- Use
Shift+F3in-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.
Load time debugging
Measure load time:
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.
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.
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.
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.
Files (roblox-suite)
-
references
-
common-bugs-and-fixes.md 4.8 KB
--- read_when: "Trace a symptom to likely causes and a focused fix" last_reviewed: 2026-06-17 --- # Common bugs and fixes ## Script silently fails Causes: - Error before any output; check Developer Console for errors. - Script in wrong context (server vs client). - Event handler disconnected or never connected. - Infinite yield from `WaitForChild` with no timeout. Fixes: - Use `instance:WaitForChild("Name", timeout)`. - Add `print` at key points to trace execution. - Check script `RunContext` and parent location. - Use `xpcall` with `debug.traceback` at entry points to capture full stack traces. ## Data not saving Causes: - DataStore call from client/LocalScript. - Non-serializable value (function, metatable, cycle, inf/nan). - Missing `pcall` around call. - Studio backend access disabled for a test experience, or enabled against production data. Fixes: - Always call DataStores from server `Script`. - Test serialization with `HttpService:JSONEncode`. - Wrap every call in `pcall`. - Do not let Studio sessions access production backend data; use a separate test experience. ## Remote events not working Causes: - Handler on wrong side. - Firing before handler is connected. - Argument types differ from handler signature. - Remote instance not replicated yet. Fixes: - Define Remotes in `ReplicatedStorage`. - Connect handlers early. - Validate argument count and types. ## Lag spikes Causes: - Pathfinding every frame. - Updating UI every frame without throttling. - Unbatched DataStore operations. - Spawning many effects/instances at once. Fixes: - Throttle expensive work. - Batch changes. - Use object pools. - Profile with MicroProfiler. ## Physics jitter Causes: - Conflicting constraints. - Wrong network ownership. - Assembly splits from anchoring. - Parts fighting each other with forces. Fixes: - Visualize constraints and ownership. - Assign vehicle ownership to driver. - Anchor only root parts of assemblies. - Tune force limits. ## Memory leaks Causes: - `Heartbeat`/`Stepped` connections not disconnected. - Tables caching instances after removal. - Animation/Sound objects not cleaned up. - Circular references with metatables. Fixes: - Disconnect connections in cleanup functions. - Use `Instance.Destroying` or `AncestryChanged` to trigger cleanup. - Check Scene Analysis → Unparented instances. - Compare Luau heap snapshots. ## UI not updating Causes: - Property change not replicating. - Layout not recalculating. - Wrong parent or ZIndex. - LocalScript not running in expected context. Fixes: - Set properties from the correct side. - Force layout updates with size/position changes. - Check StarterGui → PlayerGui cloning. ## Pathfinding failures Causes: - Agent parameters incompatible with destination. - Destination beyond 3,000 studs or vertical limits. - Dynamic obstacle blocking path. - Node budget exhausted in complex world. Fixes: - Validate parameters. - Recompute on `path.Blocked`. - Break long paths into segments. - Simplify complex geometry. ## Leaked event connections Causes: - `Heartbeat`/`Stepped` connections never disconnected. - One-shot connections left connected after firing. - Cleanup code unreachable on error paths. Fixes: - Use a Maid/Janitor-style collector. - Use `:Once()` for one-shot handlers. - Disconnect in `Instance.Destroying` or `AncestryChanged` handlers. ## spawn/delay misuse Causes: - Legacy `spawn`/`delay`/`wait` resume unpredictably and can defer indefinitely. Fixes: - Replace `spawn(fn)` with `task.defer(fn)` or `task.spawn(fn)`. - Replace `delay(t, fn)` with `task.delay(t, fn)`. - Replace `wait(t)` with `task.wait(t)`. ## Log spam and leaked secrets Causes: - Printing every frame or every event. - Logging raw player data, identifiers, or API keys. Fixes: - Rate-limit logs with a time gate or counter. - Sanitize values before logging. - Never commit secrets; load them from secure configuration. ## DataStore throttling and failures Causes: - Missing retry on transient errors. - Calling DataStores from the client. - Saving non-serializable values. Fixes: - Wrap calls with exponential backoff and jitter. - Validate JSON serialization before saving. - Always call DataStores from a server `Script`. ## Network ownership issues Causes: - Server simulating objects that a player should control. - Frequent ownership changes. Fixes: - Use `BasePart:SetNetworkOwner(player)` for vehicles and held items. - Visualize ownership in debug builds with `GetNetworkOwner()`. - Minimize ownership churn. ## Memory snapshot comparison For a repeatable leak-finding workflow, compare snapshots: 1. Capture a baseline in a stable state. 2. Perform the suspected leak action repeatedly. 3. Return to the stable state and capture again. 4. Compare PlaceMemory, Luau heap, and instance counts. 5. Check Scene Analysis → Unparented instances. -
debugging-tools.md 3.4 KB
--- read_when: "Inspect logs, breakpoints, stack traces, or runtime state" last_reviewed: 2026-06-17 --- # Debugging tools Official guides: - https://create.roblox.com/docs/en-us/studio/developer-console - https://create.roblox.com/docs/en-us/performance-optimization/microprofiler - https://create.roblox.com/docs/en-us/performance-optimization/scene-analysis ## Developer Console Shortcuts: - `F9`: open console. - `/console`: in chat. ### Log tab Filter by Output, Information, Warning, Error. Toggle Client/Server to see which side produced output. ### Memory tab - **PlaceMemory**: breakdown by assets and engine systems. - **Luau heap**: detailed script allocation snapshots. ### Network tab - Summary of HTTP and service requests. - Per-request status, time, URL, response. ### Server Stats tab - Heartbeat steps per second. - Average ping/data ping. ## Output window Studio-only output. Shows print/warn/error from edit-mode scripts. ## MicroProfiler Shortcuts: - `Ctrl+F6` (`⌘+F6`): open. - `Ctrl+P` (`⌘+P`): pause. Workflow: 1. Identify a frame-time spike. 2. Zoom into the timeline. 3. Find the widest task bar. 4. Cross-reference with the tag table. 5. Add `debug.profilebegin`/`debug.profileend` around suspicious script code. Custom profiling: ```lua debug.profilebegin("HeavyLoop") -- expensive work debug.profileend() ``` ## Scene Analysis Available in Studio under **Window → Performance Summary → Scene Analysis**. Useful views: - **Script memory**: which scripts allocate the most. - **Unparented instances**: references held by scripts after instances are removed. - **Instance composition**: total instance counts. - **Audio/Animation memory**: asset retention. - **Triangle composition**: rendering cost breakdown. Scene Analysis is a Studio UI tool only; there is no public runtime API for it. ## Script Profiler Records per-script CPU cost. Good for finding expensive scripts without manual labeling. ## Network simulation Studio: `Alt+S` or **Test → Network Simulation**. Simulate latency, jitter, and packet loss to reproduce multiplayer issues locally. ## Testing modes - **Play**: local client+server. - **Play Here**: spawn at camera. - **Multi-client simulation**: test ownership and replication with multiple local players. ## 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. ## Luau type checking Use the Luau language server (`luau-lsp`) or Studio's Luau type checker to catch errors before runtime. - Add type annotations to module exports and pure functions. - Enable `--!strict` for new modules. - Run `luau-lsp analyze` in CI when available. ## xpcall and debug.traceback Use `xpcall` with `debug.traceback` to capture full stack traces from failures: ```lua local ok, err = xpcall(riskyOperation, debug.traceback) if not ok then warn(err) end ``` Prefer `xpcall` at top-level entry points, scheduled callbacks, and connection handlers. ## Visual debugging Use `DebugDraw` (see scripts) or temporary `Part` instances to visualize: - Raycast hits - Hitboxes - Path waypoints - Sensor ranges - Network ownership boundaries Always clean up debug visuals in production. -
network-simulation.md 1.8 KB
--- read_when: "Reproduce latency, jitter, or packet-loss problems in Studio" last_reviewed: 2026-09-19 --- # Studio network simulation Official source: https://create.roblox.com/docs/studio/network-simulator Network Simulator is a Studio beta that affects playtests, not live players. Enable **New Device Simulator** under **File → Beta Features**, restart Studio, and open **Test → Device Simulator → Network**. - Inbound means server to client; outbound means client to server. Configure latency, jitter, and packet loss independently for each direction. - Preset selection and numeric edits are staged until **Apply**. Applying updates a running playtest immediately. - The values add to existing conditions, including real internet latency in Team Test. Inbound and outbound delays both contribute to round-trip ping. - **Ideal Fiber** adds 8 ms per direction. Set all six controls to zero and apply for a zero-added-impairment comparison. **Reset** stages Ideal Fiber; it does not disable simulation. - Packet loss is a percentage: `0.50%` means half a percent, not 50%. - Saved custom presets persist across sessions, but applied values reset after a full Studio restart. Repeat the same server-confirmed interaction under baseline, Wi-Fi/mobile, and poor-connection conditions. Check immediate local feedback, duplicate submissions, timeouts, stale unreliable updates, and recovery. Change one direction at a time to isolate request versus response delays. Record the preset, playtest mode, and observed result so comparisons remain reproducible. The toolbar uses the same `NetworkSettings` properties as Studio Settings: `InboundNetworkMinDelayMs`, `OutboundNetworkMinDelayMs`, `InboundNetworkJitterMs`, `OutboundNetworkJitterMs`, `InboundNetworkLossPercent`, and `OutboundNetworkLossPercent`. Consult their security requirements before using them in plugin automation. -
performance-profiling.md 2.8 KB
--- read_when: "Find CPU, GPU, memory, or load-time bottlenecks" last_reviewed: 2026-06-17 --- # Performance profiling Official guide: https://create.roblox.com/docs/en-us/performance-optimization/identify ## Frame time goals | FPS | Frame time | | --- | --- | | 30 | 33.33 ms | | 60 | 16.67 ms | | 120 | 8.33 ms | | 240 | 4.17 ms | Consistent frame times matter more than average FPS. ## Server heartbeat Capped at 60 FPS. Check in Developer Console → Server Stats → Heartbeat → Steps Per Sec. If below 60, use MicroProfiler on the server to find the bottleneck. ## Client frame rate Use `Shift+F5` in-game for debug stats, or test on a real mobile device. High-end PCs can hide performance problems due to thermal/power headroom. ## MicroProfiler colors - **Orange**: worker thread bottleneck (scripts, physics, pathfinding, animations). - **Blue**: render thread bottleneck (geometry, effects, UI). - **Red**: GPU wait (complex geometry, large textures, overdrawing). ## Server profiling 1. Join a live or test game with edit permissions. 2. Open Developer Console (`F9`). 3. Switch to **MicroProfiler** tab. 4. Choose Server, set frames and delay. 5. Click **Begin server recording**. 6. Open the saved HTML dump. ## Common bottlenecks | Area | Typical causes | | --- | --- | | Scripts | Loops every frame, unbatched datastore calls, expensive string/table ops | | Physics | Too many unanchored parts, conflicting constraints, no sleep | | Rendering | Too many transparent parts, particles, lights, high-poly meshes | | Memory | Unloaded assets, leaked instances, growing tables | | Network | Excessive remote traffic, large replication payloads | ## Optimization workflow 1. Measure with MicroProfiler/Scene Analysis. 2. Identify the biggest single cost. 3. Change one thing. 4. Re-measure. 5. Repeat. ## Mobile profiling 1. Open MicroProfiler on mobile from Settings. 2. Note the IP:port displayed. 3. On a dev machine on the same network, open that IP:port in a browser. 4. Capture frames and analyze in the web UI. ## Counters mode and flame graphs The web MicroProfiler supports: - **Flame graphs**: aggregated call stacks. - **Diff flame graphs**: compare two dumps. - **X-Ray memory**: highlight allocation-heavy frames. Use these for regressions and long-term tracking. ## Memory snapshot comparison workflow Use the Developer Console Memory tab and Scene Analysis to find leaks: 1. Enter a stable baseline state (e.g., standing in lobby with no UI open). 2. Capture snapshot A from Developer Console → Memory or Scene Analysis. 3. Perform the suspected leak action (open/close UI, spawn/despawn enemies). 4. Return to the baseline state. 5. Capture snapshot B. 6. Compare: - Total PlaceMemory - Luau heap by script - Instance counts by category - Unparented instances in Scene Analysis 7. Investigate categories that did not return to baseline. -
testing-patterns.md 2.7 KB
--- read_when: "Write unit tests, mock dependencies, or test client-server flows" last_reviewed: 2026-06-17 --- # Testing patterns Official guide: https://roblox.github.io/testez/ ## TestEZ The standard Roblox testing framework is [TestEZ](https://github.com/Roblox/testez). It supports nested `describe`/`it` blocks, lifecycle hooks, async tests, and a rich matcher API. ```lua local ReplicatedStorage = game:GetService("ReplicatedStorage") local TestEZ = require(ReplicatedStorage.DevPackages.TestEZ) TestEZ.TestBootstrap:run({ game.ReplicatedStorage.Tests }) ``` ## When to write tests - Pure utility functions (math, formatting, validation). - Data transformation and serialization logic. - State machines with deterministic transitions. - Anything that has broken before and must not break again. ## Test structure with TestEZ ```lua describe("MathUtils", function() local clamp beforeEach(function() clamp = require(ReplicatedStorage.MathUtils).clamp end) it("clamps values", function() expect(clamp(5, 0, 10)).to.equal(5) expect(clamp(-1, 0, 10)).to.equal(0) expect(clamp(11, 0, 10)).to.equal(10) end) end) ``` TestEZ matchers include `to.equal`, `to.be.near`, `to.be.a`, `to.throw`, `to.be.ok`, `to.be.truthy`, and more. ## Example async test ```lua it("loads data asynchronously", function() return expect(Promise.resolve(42)).to.equal(42) end) ``` For tests that return a Promise, TestEZ waits for resolution. See TestEZ documentation for the exact async API. ## Integration vs unit tests - **Unit tests** run fast and don't need a running game. - **Integration tests** require services, instances, or network state. Run them in Studio play mode. ## TestService (legacy) `TestService:Error(msg)` marks a test failure visibly in Studio output. It is useful for simple in-Studio checks, but new projects should prefer TestEZ. ```lua local TestService = game:GetService("TestService") local ok, err = pcall(someTest) if not ok then TestService:Error(err) end ``` ## Running tests - Place TestEZ test scripts under `ReplicatedStorage.Tests` or another discoverable folder. - Use `TestEZ.TestBootstrap:run({ folder })` from a single entry point. - For CI-style testing, collect all test modules and run them with a single runner. - The scripts/TestRunner.lua module is a minimal shim for environments where TestEZ is unavailable. ## Good test habits - One assertion per behavior. - Test edge cases (empty tables, nil, huge values). - Keep tests deterministic; avoid randomness. - Clean up any instances created during tests. - Use `beforeEach`/`afterEach` for shared setup and teardown. - Mock engine services for unit tests instead of relying on the live data model.
-
-
scripts
-
DebugDraw.lua 2.8 KB · in bundle
-
Logger.lua 2 KB · in bundle
-
TestRunner.lua 11.1 KB · in bundle
-
-
SKILL.md 11.9 KB
--- name: roblox-testing description: "Test, debug, or profile Roblox experiences. Use for Studio debugging, TestEZ, logging, connection cleanup, memory or network diagnostics, MicroProfiler, Scene Analysis, and Script Profiler." last_reviewed: 2026-06-17 --- # 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. ## 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. ## 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. ## pcall and assertions 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") ``` ## 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. ## Unit testing with TestEZ The standard Roblox testing framework is [TestEZ](https://github.com/Roblox/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. ## Luau type checking Use the [Luau language server](https://luau.org/) (`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. ## 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. ## xpcall and 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. ## 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. ## Prefer task over 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. ## 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 ``` ## 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. ## DataStore retry pattern 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. ## 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. ## 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. ## 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. ## Script Profiler Records CPU time per script. Use it when MicroProfiler points to scripts but you need to know which script. ## Common bug categories | Symptom | Likely causes | | --- | --- | | Script silently fails | Missing `pcall`, error swallowed, wrong script context | | Data not saving | DataStore called from client, non-serializable value, no `pcall` | | 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 | ## 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](references/network-simulation.md). ## 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. ## 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. ## 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 --> ## Reference index - [common-bugs-and-fixes.md](references/common-bugs-and-fixes.md): Trace a symptom to likely causes and a focused fix. - [debugging-tools.md](references/debugging-tools.md): Inspect logs, breakpoints, stack traces, or runtime state. - [network-simulation.md](references/network-simulation.md): Reproduce latency, jitter, or packet-loss problems in Studio. - [performance-profiling.md](references/performance-profiling.md): Find CPU, GPU, memory, or load-time bottlenecks. - [testing-patterns.md](references/testing-patterns.md): Write unit tests, mock dependencies, or test client-server flows. <!-- catalog:references:end -->
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.