Claude Skill

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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download nonlooped-roblox-suite-roblox-testing-2904c16.zip · 18 KB
Part of nonlooped/roblox-suite — 15 skills

Install

skills CLI npx skills add https://github.com/nonlooped/roblox-suite/tree/main/roblox-testing
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install nonlooped-roblox-suite@llmmart
Git 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:

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:

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 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:

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.Destroying to 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:

  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.

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

  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.

Reference index

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.

No comments yet.

Reviews (0)

No reviews yet.

Related