Home › Tips
Testing
Flux is designed for fast, deterministic testing: components are plain functions, state lives in standalone nodes, and Flux.Clock lets you step animations, timers, and effects frame-by-frame with zero real-time waits.
Deterministic Effects with Flux.flush()
Because Effects and property bindings batch their updates to the end of the frame, call Flux.flush() in synchronous unit tests to run all queued effects immediately:
local count = Flux.signal(0)
local label = Flux.new "TextLabel" {
Text = function()
return `Count: {count()}`
end,
}
-- Initial property value is applied synchronously on creation
assert(label.Text == "Count: 0")
count(5)
Flux.flush() -- flush pending effects and property bindings now
assert(label.Text == "Count: 5")Virtual Time with Flux.Clock
Flux.Clock is the single source of time for Flux's scheduler, animations (Flux.spring, Flux.tween), and Flux.presence.
In production on Roblox, Flux.Clock delegates directly to os.clock(), workspace:GetServerTimeNow(), task.*, and RunService. In tests (or headless Luau, which is always in manual mode), enable manual mode with Flux.Clock.manual(true) so time advances only when you call Flux.Clock.step(seconds):
local Clock = Flux.Clock
Clock.manual(true)
local x = Flux.spring(0, 4, 1)
x(100)
-- Advance 0.5 seconds in 1/60s frames (steps springs, tweens, timers, and flushes effects)
Clock.step(0.5)
assert(x() > 50 and x() < 100)
-- Advance another 1.5 seconds until settled at rest
Clock.step(1.5)
assert(x() == 100)
Clock.reset() -- clear any pending timers/waits/deferred tasks between testsFlux.Clock API
| Function | Description |
|---|---|
Clock.now() | Monotonic time in seconds (os.clock() live, virtual clock in manual mode). |
Clock.wall() | Synchronized Unix timestamp (GetServerTimeNow() live, 2026-01-01T00:00:00Z + now manual). |
Clock.delay(seconds, fn, ...) | Schedules fn after seconds (task.delay live, virtual heap in manual mode). |
Clock.wait(seconds?) | Yields the current coroutine for seconds and returns elapsed time. |
Clock.defer(fn, ...) | Defers fn to the end of the current resumption cycle (task.defer live). |
Clock.cancel(thread) | Cancels a thread scheduled with Clock.delay or Clock.defer. |
Clock.connect(phase, fn) | Connects fn(dt) to "PreRender", "RenderStepped", or "Heartbeat". |
Clock.manual(enabled) | Switches between the live Roblox engine clock (false) and the manual virtual clock (true). |
Clock.isManual() | Returns true when the clock is in manual mode. |
Clock.step(seconds, frameLength?) | Advances manual time by seconds in slices of at most frameLength (default 1/60). |
Clock.drain() | Runs all pending Clock.defer callbacks immediately in FIFO order. |
Clock.reset() | Cancels all pending manual timers, waits, deferred calls, and manual frame listeners between tests. |
Each manual frame of dt seconds executes in deterministic engine order:
- Advances
nowbydtand wakes duedelayandwaitthreads in deadline order. - Fires
"PreRender"and"RenderStepped"listeners withdt. - Steps all active springs and tweens by
dt(Flux.Motion.step(dt)) and runsFlux.flush(). - Fires
"Heartbeat"listeners withdt. - Drains
Clock.deferqueues between each phase.
If you want to advance springs and tweens in isolation without stepping timers or Clock listeners, call Flux.Motion.step(dt) followed by Flux.flush() directly.
Isolating Tests with Scopes
Wrap each test body in Flux.scope and destroy the scope at the end of the test so effects, animations, and instances never leak between test cases:
local scope = Flux.scope(function()
local count = Flux.signal(0)
local doubled = Flux.computed(function()
return count() * 2
end)
assert(doubled() == 0)
count(5)
assert(doubled() == 10)
end)
scope:Destroy()Headless & Full-Engine Tests in CI
The reactive core (Flux.signal, Flux.computed, Flux.effect, Flux.scope, Flux.store, Flux.Clock, Flux.Color) and most motion datatypes (number, vector, UDim, UDim2, Vector2, Vector3, Color3, NumberSequence, ColorSequence) run directly in the standalone luau CLI via built-in compatibility shims.
For tests that create Roblox Instances or use full-engine APIs (CFrame, Rect, NumberRange, CollectionService, input, and layout geometry), build a test place with Rojo and run the suite in Roblox Studio or in CI via the Open Cloud Luau Execution API.