git:20260919.e20f73c to git:20261021.283d125

2 added, 2 removed. Audit A to A.

---
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
+ last_reviewed: 2026-10-21
---
# 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.
+ Run this from a `LocalScript` in `ReplicatedFirst`. It starts timing when `ReplicatedFirst` replicates and stops when `game.Loaded` fires, so it excludes the time to connect to the server and load the avatar. Load time as players experience it, from clicking **Play** to the world rendering, is longer. 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 -->