Skip to content

Studio and testing

The store option studio decides what a Studio play test does with data. Live servers ignore it.

const store = KeepBlox.store("Profiles", {
template = TEMPLATE,
studio = "memory",
})
Value What happens
"live" (default) The real data store when the place has API access, memory when it has none.
"memory" Always memory. Nothing live is read or written.
"copy" Each profile starts as a copy of its live data, read once and never written, then lives in memory.

"copy" is for testing against real profiles without touching them. The copy is taken without the live record’s lock, so a play test can load a key that a live server holds. It needs API access to read the live data; a key that cannot be read starts from the template.

With studio = "live" in a place without API access, the first load makes one read-only call (GetAsync of the key __KeepBlox_access_check). When it fails for lack of access, the store switches to memory for the rest of the play test, and store.onError fires once with the key "" and the message:

Studio has no API access: profiles are kept in memory, and nothing is saved

The check reads and never writes, so it cannot touch a live key.

mock keeps a store’s profiles in memory, anywhere, not only in Studio:

const store = KeepBlox.store("Profiles", { template = TEMPLATE, mock = true })
const other = KeepBlox.store("Profiles", { template = TEMPLATE, mock = "test-trades" })
  • mock = true uses the scratch set "scratch"; a string names the set.
  • Each set keeps its own data stores, ordered data stores (for leaderboards), messaging and MemoryStore, for the lifetime of the server. Stores opened with the same set name see the same data; different names stay apart.
  • Values are deep-copied on every write and read, as a real store serializes them, so test code can never hold a reference into stored data.
  • Nothing in a scratch set ever reaches a live key.
  • mock takes precedence over studio.
  • A scratch set keeps no versions: store:versions, store:readVersion and store:restore raise an error.

KeepBlox.shared takes mock too; its default set is "shared".

On a live server, KeepBlox binds to game:BindToClose and releases every open profile when the server shuts down. store:close() runs the same code on demand:

const result = store:load("u_1")
assert(result.ok)
const profile = result.profile
local endedWith = nil
profile.onEnded:connect(function(reason)
endedWith = reason
end)
profile.Data.coins = 50
store:close()
assert(not profile:isActive())
assert(store:load("u_2").reason == "closing")

store:close():

  • marks the store closing, so every new load fails with "closing";
  • releases every open profile in parallel, each with a final save, ending with the reason "shutdown";
  • yields until all are released or shutdownDeadline seconds (default 25) pass.

onEnded listeners run in their own threads, so check endedWith after they had a chance to run.

KeepBlox.migrate(options, data, from) runs a store’s migrations, defaults and schema check on a copy of data stored at schema version from, exactly as a load does. Use it with fixtures from before and after each migration.

const outcome = KeepBlox.migrate(PROFILE_OPTIONS, { gold = 5 }, 0)
assert(outcome.ok)
assert(outcome.data.coins == 5 and outcome.data.gold == nil)
assert(outcome.changed)
const newer = KeepBlox.migrate(PROFILE_OPTIONS, { coins = 5 }, 99)
assert(not newer.ok and newer.reason == "newerSchema")

It returns { ok = true, data, changed }, where changed is true when from differs from the current version, or { ok = false, reason, message } with the reason "newerSchema", "migration" or "schema" (see Load failures). The data you pass is not changed. Pass the same schema, version and migrations the store uses; see Schema and migrations.

tests/live runs KeepBlox’s guarantees on Roblox’s real services, in a private test experience (the one named in roblox.env.example; it refuses to run anywhere else). luneblox run tests/live/Serve serves it; Load.luau in Studio’s command bar builds ServerStorage.KB_LiveCheck; Start.luau on the play server runs the functional check (about 5 minutes) or the load test (about 15). Cleanup.luau removes what it left, from Edit mode too: every live key of every KB_LiveCheck_* data store (several at once, paced to the server’s budget), the runs’ ordered stores and the MemoryStore entries of its virtual servers, then lists again and reports what is left, which must be nothing (974 keys took 4 minutes). In a live game, run it slowly (20 removes a minute): the experience’s limit is shared with the players’ saves. A removed key stops counting toward the experience’s storage at once; Roblox keeps its old versions for 30 days, outside the limit, and nothing can remove them sooner. See tests/live/README.md.

The store option services replaces the engine services behind the store. KeepBlox’s own test suite uses it to run many virtual servers with fake DataStore, MemoryStore and MessagingService under virtual time. A game rarely needs it: mock covers testing inside Roblox.