Skip to main content

Specium

A lightweight and flexible testing framework for Roblox.

Specium allows you to organize tests into suites and sub-suites, run them and get a detailed report of the results.

Types

SpeciumResult

interface SpeciumResult {
successboolean--

Whether the test passed or failed

messagestring--

A message describing the result

}

SpeciumRunResult

interface SpeciumRunResult {
totalnumber--

Total number of tests run

passednumber--

Number of tests that passed

failednumber--

Number of tests that failed

suites{SpeciumSuiteResult}--

Results for each root suite

}

Functions

suite

Specium.suite(
namestring,--

The name of the suite

fn(SpeciumSuiteContext) → ()--

A function that registers tests and sub-suites

) → SpeciumSuite--

The constructed suite, ready to be passed to Specium.run

Types

interface SpeciumSuiteContext {
it(
namestring,
fn() → SpeciumResult,
timeoutnumber?
) → ()--

Registers a test case inside the current suite

skip(
namestring,
fn() → SpeciumResult,
timeoutnumber?
) → ()--

Registers a test case inside the current suite

describe(
namestring,
fn(SpeciumSuiteContext) → ()
) → ()--

Creates a nested sub-suite

beforeAll(() → ())?--

Runs once before all tests in the suite

afterAll(() → ())?--

Runs once after all tests in the suite

beforeEach(() → ())?--

Runs before each test in the suite

afterEach(() → ())?--

Runs after each test in the suite

}

Creates a new test suite.

Example:

local suite = Specium.suite("Math", function(t)
	t.it("adds numbers", function()
		Specium.expect(1 + 1).toBe(2)
		return Specium.success("ok")
	end)

	t.describe("subtraction", function(t)
		t.it("subtracts numbers", function()
			Specium.expect(5 - 3).toBe(2)
			return Specium.success("ok")
		end)
	end)
end)

run

Specium.run(
suiteSpeciumSuite--

The suite to run

) → (
string,--

A formatted output string with a tree-view of results and a summary

SpeciumRunResult--

A structured table with the full results

)

Runs all tests in a suite (including sub-suites).

Example:

local output, results = Specium.run(suite)
print(output)
-- > Math
--   [Pass] adds numbers - ok
--   > subtraction
--     [Pass] subtracts numbers - ok
--
-- Result:
-- 2/2 (100%) tests passed

runTests

Specium.runTests(
testsFolder{Instance} | Instance--

A single folder or a list of folders to scan for .spec ModuleScripts.

) → (
string,--

A concatenated output string of all suite results

{SpeciumRunResult}--

A list of results, one per suite found

)

Scans one or more folders for .spec ModuleScripts and runs each one as a suite.

Each .spec module must return a SpeciumSuite created via Specium.suite. If a module fails to load, a warning is emitted and it is skipped.

Example:

-- Single folder
local output, results = Specium.runTests(ServerScriptService.Tests)
print(output)

-- Multiple folders
local output, results = Specium.runTests({
	ServerScriptService.Tests,
	ReplicatedStorage.Shared.Tests,
})
print(output)

error

Specium.error(
messagestring--

The failure message

) → SpeciumResult--

A result with success = false

Returns a failed SpeciumResult with the given message.

Example:

t.it("always fails", function()
	return Specium.error("this test is not implemented yet")
end)

success

Specium.success(
messagestring--

The success message

) → SpeciumResult--

A result with success = true

Returns a successful SpeciumResult with the given message.

Example:

-- Returning Specium.success is optional — if omitted, the test is marked as passed automatically.
t.it("works", function()
	Specium.expect(1 + 1).toBe(2)
	return Specium.success("all good")
end)

expect

Specium.expect(
receivedany--

The value to assert against

) → SpeciumMatchers--

A table of matcher functions

Creates a matcher chain for the given value.

Use .never to invert any matcher, and .withMessage(msg) to prepend a custom message to any failure.

Available matchers:

  • toBe(expected) — strict equality (==)
  • toEqual(expected) — deep equality (tables compared recursively)
  • toBeA(type, legacy?) — checks typeof (or type if legacy is true)
  • toBeTruthy() — value is not nil and not false
  • toBeNil() — value is nil
  • toBeNear(expected, epsilon?) — numeric proximity (default epsilon: 1e-5)
  • toBeGreaterThan(expected) — numeric >
  • toBeLessThan(expected) — numeric <
  • toContain(item) — table contains value (deep) or string contains substring
  • toHaveLength(expected) — checks #value
  • toMatch(pattern) — string matches a Lua pattern
  • toThrow(expected?, ...args) — function throws when called with args
  • withMessage(msg) — prepends a custom message to any failure
  • never — inverts the next matcher

Example:

-- All examples below pass
Specium.expect(2 + 2).toBe(4)
Specium.expect("hello world").toContain("world")
Specium.expect({1, 2, 3}).toHaveLength(3)
Specium.expect(math.huge).never.toBeNil()
Specium.expect(function() error("oops") end).toThrow()
Specium.expect(42).withMessage("answer should be 42").toBe(42)
Specium.expect(1 + 1).never.toBe(3)
Show raw api
{
    "functions": [
        {
            "name": "suite",
            "desc": "Creates a new test suite.\n\nExample:\n```lua\nlocal suite = Specium.suite(\"Math\", function(t)\n\tt.it(\"adds numbers\", function()\n\t\tSpecium.expect(1 + 1).toBe(2)\n\t\treturn Specium.success(\"ok\")\n\tend)\n\n\tt.describe(\"subtraction\", function(t)\n\t\tt.it(\"subtracts numbers\", function()\n\t\t\tSpecium.expect(5 - 3).toBe(2)\n\t\t\treturn Specium.success(\"ok\")\n\t\tend)\n\tend)\nend)\n```",
            "params": [
                {
                    "name": "name",
                    "desc": "The name of the suite",
                    "lua_type": "string"
                },
                {
                    "name": "fn",
                    "desc": "A function that registers tests and sub-suites",
                    "lua_type": "(SpeciumSuiteContext) -> ()"
                }
            ],
            "returns": [
                {
                    "desc": "The constructed suite, ready to be passed to `Specium.run`",
                    "lua_type": "SpeciumSuite"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 107,
                "path": "lib/init.luau"
            }
        },
        {
            "name": "run",
            "desc": "Runs all tests in a suite (including sub-suites).\n\nExample:\n```lua\nlocal output, results = Specium.run(suite)\nprint(output)\n-- > Math\n--   [Pass] adds numbers - ok\n--   > subtraction\n--     [Pass] subtracts numbers - ok\n--\n-- Result:\n-- 2/2 (100%) tests passed\n```",
            "params": [
                {
                    "name": "suite",
                    "desc": "The suite to run",
                    "lua_type": "SpeciumSuite"
                }
            ],
            "returns": [
                {
                    "desc": "A formatted output string with a tree-view of results and a summary",
                    "lua_type": "string"
                },
                {
                    "desc": "A structured table with the full results",
                    "lua_type": "SpeciumRunResult"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 187,
                "path": "lib/init.luau"
            }
        },
        {
            "name": "runTests",
            "desc": "Scans one or more folders for `.spec` ModuleScripts and runs each one as a suite.\n\nEach `.spec` module must return a `SpeciumSuite` created via `Specium.suite`.\nIf a module fails to load, a warning is emitted and it is skipped.\n\nExample:\n```lua\n-- Single folder\nlocal output, results = Specium.runTests(ServerScriptService.Tests)\nprint(output)\n\n-- Multiple folders\nlocal output, results = Specium.runTests({\n\tServerScriptService.Tests,\n\tReplicatedStorage.Shared.Tests,\n})\nprint(output)\n```",
            "params": [
                {
                    "name": "testsFolder",
                    "desc": "A single folder or a list of folders to scan for `.spec` ModuleScripts.",
                    "lua_type": "{Instance} | Instance"
                }
            ],
            "returns": [
                {
                    "desc": "A concatenated output string of all suite results",
                    "lua_type": "string"
                },
                {
                    "desc": "A list of results, one per suite found",
                    "lua_type": "{SpeciumRunResult}"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 356,
                "path": "lib/init.luau"
            }
        },
        {
            "name": "error",
            "desc": "Returns a failed `SpeciumResult` with the given message.\n\nExample:\n```lua\nt.it(\"always fails\", function()\n\treturn Specium.error(\"this test is not implemented yet\")\nend)\n```",
            "params": [
                {
                    "name": "message",
                    "desc": "The failure message",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "A result with `success = false`",
                    "lua_type": "SpeciumResult"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 398,
                "path": "lib/init.luau"
            }
        },
        {
            "name": "success",
            "desc": "Returns a successful `SpeciumResult` with the given message.\n\nExample:\n```lua\n-- Returning Specium.success is optional — if omitted, the test is marked as passed automatically.\nt.it(\"works\", function()\n\tSpecium.expect(1 + 1).toBe(2)\n\treturn Specium.success(\"all good\")\nend)\n```",
            "params": [
                {
                    "name": "message",
                    "desc": "The success message",
                    "lua_type": "string"
                }
            ],
            "returns": [
                {
                    "desc": "A result with `success = true`",
                    "lua_type": "SpeciumResult"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 418,
                "path": "lib/init.luau"
            }
        },
        {
            "name": "expect",
            "desc": "Creates a matcher chain for the given value.\n\nUse `.never` to invert any matcher, and `.withMessage(msg)` to prepend\na custom message to any failure.\n\nAvailable matchers:\n- `toBe(expected)` — strict equality (`==`)\n- `toEqual(expected)` — deep equality (tables compared recursively)\n- `toBeA(type, legacy?)` — checks `typeof` (or `type` if `legacy` is true)\n- `toBeTruthy()` — value is not `nil` and not `false`\n- `toBeNil()` — value is `nil`\n- `toBeNear(expected, epsilon?)` — numeric proximity (default epsilon: `1e-5`)\n- `toBeGreaterThan(expected)` — numeric `>`\n- `toBeLessThan(expected)` — numeric `<`\n- `toContain(item)` — table contains value (deep) or string contains substring\n- `toHaveLength(expected)` — checks `#value`\n- `toMatch(pattern)` — string matches a Lua pattern\n- `toThrow(expected?, ...args)` — function throws when called with `args`\n- `withMessage(msg)` — prepends a custom message to any failure\n- `never` — inverts the next matcher\n\nExample:\n```lua\n-- All examples below pass\nSpecium.expect(2 + 2).toBe(4)\nSpecium.expect(\"hello world\").toContain(\"world\")\nSpecium.expect({1, 2, 3}).toHaveLength(3)\nSpecium.expect(math.huge).never.toBeNil()\nSpecium.expect(function() error(\"oops\") end).toThrow()\nSpecium.expect(42).withMessage(\"answer should be 42\").toBe(42)\nSpecium.expect(1 + 1).never.toBe(3)\n```",
            "params": [
                {
                    "name": "received",
                    "desc": "The value to assert against",
                    "lua_type": "any"
                }
            ],
            "returns": [
                {
                    "desc": "A table of matcher functions",
                    "lua_type": "SpeciumMatchers"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 460,
                "path": "lib/init.luau"
            }
        }
    ],
    "properties": [],
    "types": [
        {
            "name": "SpeciumResult",
            "desc": "",
            "fields": [
                {
                    "name": "success",
                    "lua_type": "boolean",
                    "desc": "Whether the test passed or failed"
                },
                {
                    "name": "message",
                    "lua_type": "string",
                    "desc": "A message describing the result"
                }
            ],
            "source": {
                "line": 42,
                "path": "lib/init.luau"
            }
        },
        {
            "name": "SpeciumRunResult",
            "desc": "",
            "fields": [
                {
                    "name": "total",
                    "lua_type": "number",
                    "desc": "Total number of tests run"
                },
                {
                    "name": "passed",
                    "lua_type": "number",
                    "desc": "Number of tests that passed"
                },
                {
                    "name": "failed",
                    "lua_type": "number",
                    "desc": "Number of tests that failed"
                },
                {
                    "name": "suites",
                    "lua_type": "{SpeciumSuiteResult}",
                    "desc": "Results for each root suite"
                }
            ],
            "source": {
                "line": 54,
                "path": "lib/init.luau"
            }
        },
        {
            "name": "SpeciumSuiteContext",
            "desc": "",
            "fields": [
                {
                    "name": "it",
                    "lua_type": "(name: string, fn: () -> SpeciumResult, timeout: number?) -> ()",
                    "desc": "Registers a test case inside the current suite"
                },
                {
                    "name": "skip",
                    "lua_type": "(name: string, fn: () -> SpeciumResult, timeout: number?) -> ()",
                    "desc": "Registers a test case inside the current suite"
                },
                {
                    "name": "describe",
                    "lua_type": "(name: string, fn: (SpeciumSuiteContext) -> ()) -> ()",
                    "desc": "Creates a nested sub-suite"
                },
                {
                    "name": "beforeAll",
                    "lua_type": "(() -> ())?",
                    "desc": "Runs once before all tests in the suite"
                },
                {
                    "name": "afterAll",
                    "lua_type": "(() -> ())?",
                    "desc": "Runs once after all tests in the suite"
                },
                {
                    "name": "beforeEach",
                    "lua_type": "(() -> ())?",
                    "desc": "Runs before each test in the suite"
                },
                {
                    "name": "afterEach",
                    "lua_type": "(() -> ())?",
                    "desc": "Runs after each test in the suite"
                }
            ],
            "source": {
                "line": 72,
                "path": "lib/init.luau"
            }
        }
    ],
    "name": "Specium",
    "desc": "A lightweight and flexible testing framework for Roblox.\n\nSpecium allows you to organize tests into suites and sub-suites,\nrun them and get a detailed report of the results.",
    "source": {
        "line": 8,
        "path": "lib/init.luau"
    }
}