- Luau 98.1%
- Nix 1.9%
| .forgejo/workflows | ||
| .github | ||
| .vscode | ||
| docs | ||
| src | ||
| tests | ||
| .editorconfig | ||
| .gitignore | ||
| .luaurc | ||
| bleumoon.lock | ||
| bleumoon.toml | ||
| CHANGELOG.md | ||
| flake.lock | ||
| flake.nix | ||
| init.luau | ||
| LICENSE | ||
| README.md | ||
| stylua.toml | ||
bleutest
A comprehensive test suite library for Luau on the BleuMoon runtime.
bleutest provides a batteries-included testing experience: a BDD-style test runner, a fluent assertion API with 30+ built-in matchers, mocking & spying utilities, multiple reporter formats, and automatic test file discovery.
Features
- BDD test runner —
describe/it/testwithbeforeAll,afterAll,beforeEach,afterEachhooks - Fluent assertions —
expect(value).to.equal(42),expect(fn).to.throw(),expect(value).never.to.beNil() - 30+ matchers — equality, truthiness, type checks, numeric comparisons, string patterns, table inspection, error handling, and mock verification
- Mocking & spying —
mock.fn(),mock.spy(),mock.stub(),mock.spyOn()with full call tracking - Skip & focus —
it.skip()/describe.skip()andit.only()/describe.only() - Multiple reporters — colored console (default), TAP v14, and JSON
- Auto-discovery — glob-based test file discovery
- Extensible — register custom matchers with
registerMatcher()
Quick Start
Installation
Add bleutest as a dependency in your project's bleumoon.toml:
[dev-dependencies]
bleutest = "^0.1.0"
Then install:
bleumoon pkg install
Writing Tests
Create a test file (e.g. tests/math.test.luau):
local bleutest = require("@pkg/bleutest")
local describe = bleutest.describe
local it = bleutest.it
local expect = bleutest.expect
describe("arithmetic", function()
it("adds two numbers", function()
expect(1 + 1).to.equal(2)
end)
it("multiplies correctly", function()
expect(3 * 4).to.beGreaterThan(10)
end)
end)
Running Tests
Create a test entry point (e.g. tests/init.luau):
local bleutest = require("@pkg/bleutest")
-- Require your test files so they register with the runner
require("./tests/math.test")
-- Run all registered tests (auto-creates a console reporter)
local results = bleutest.run({ reporter = "console" })
local process = require("@bleumoon/process")
process.exit(if results.failed > 0 then 1 else 0)
Then run:
bleumoon run tests/init
API Reference
Test Registration
| Function | Description |
|---|---|
describe(name, fn) |
Register a test suite |
describe.skip(name, fn) |
Register a skipped suite |
describe.only(name, fn) |
Register a focused suite (only focused tests run) |
it(name, fn) / test(name, fn) |
Register a test case |
it.skip(name, fn) |
Register a skipped test |
it.only(name, fn) |
Register a focused test |
Lifecycle Hooks
| Function | Description |
|---|---|
beforeAll(fn) |
Run once before all tests in the current suite |
afterAll(fn) |
Run once after all tests in the current suite |
beforeEach(fn) |
Run before each test in the current suite (inherited by nested suites) |
afterEach(fn) |
Run after each test in the current suite (inherited by nested suites) |
Assertions
All assertions use the fluent expect() API:
expect(value).to.equal(42)
expect(value).never.to.beNil()
The .to property is optional syntactic sugar. .never negates the assertion.
Available Matchers
Equality
equal(expected)— shallow equality (==)deepEqual(expected)— recursive deep equalitystrictEqual(expected)— reference equality (rawequal)
Truthiness
beTruthy()— value is truthy (notnilorfalse)beFalsy()— value is falsy (nilorfalse)beNil()— value isnilbeNotNil()— value is notnil
Type Checks
beType(typeName)—typeof(value) == typeNamebeString(),beNumber(),beBoolean(),beTable(),beFunction(),beThread(),beBuffer()— shorthand type matchers
Numeric
beCloseTo(expected, tolerance?)— within ±tolerance (default1e-9)beGreaterThan(n),beGreaterThanOrEqual(n)beLessThan(n),beLessThanOrEqual(n)
String
contain(substring)— string contains substringstartWith(prefix)— string starts with prefixendWith(suffix)— string ends with suffixmatch(pattern)— Luau pattern matchmatchRegex(pattern)— regex match (via@bleumoon/regex)
Table
haveLength(n)—#value == nhaveKey(key)— table contains keyhaveValue(val)— table contains value (deep equality)containSubset(subset)— table contains all key/value pairs from subsetbeEmpty()— table has no entries
Error
throw()— function throws any errorthrowMatch(pattern)— function throws error matching a Luau pattern
Mock Verification
haveBeenCalled()— mock was called at least oncehaveBeenCalledTimes(n)— mock was called exactlyntimeshaveBeenCalledWith(...)— mock was called with the given arguments (any call)
Mocking
local mock = bleutest.mock
-- Create a mock function
local fn = mock.fn()
fn("hello")
expect(fn).to.haveBeenCalledWith("hello")
-- Configure return values
fn:returns(42)
expect(fn()).to.equal(42)
-- Configure to throw
fn:throws("oops")
expect(fn).to.throw()
-- Custom implementation
fn:implementation(function(x) return x * 2 end)
-- Spy on an existing function (calls through)
local spy = mock.spy(originalFn)
-- Stub a method on an object
local stubbed = mock.stub(obj, "method", replacementFn)
stubbed.restore() -- restore original
-- Spy on a method (calls through, records calls)
local spy = mock.spyOn(obj, "method")
Runner
-- Run with default options
local results = bleutest.run()
-- Run with options
local results = bleutest.run({
reporter = "tap", -- "console" | "tap" | "json"
filter = "math", -- only run tests whose name contains this string
timeout = 5, -- per-test timeout in seconds (default: 10)
})
-- Reset runner state (useful for isolated sub-runs)
bleutest.reset()
Reporter Factory
local reporter = bleutest.createReporter("console") -- "console" | "tap" | "json"
local results = bleutest.run(nil, reporter)
Test Discovery
-- Discover test file paths
local paths = bleutest.discover("tests/")
-- Discover with custom glob pattern
local paths = bleutest.discover("tests/", "**/*.spec.luau")
-- Discover and require all test files
-- NOTE: discoverAndRequire resolves require() paths relative to the bleutest
-- package directory, not the consuming project. For consuming projects, use
-- discover() with a manual require loop instead (see the Discovery docs).
local errors = bleutest.discoverAndRequire("tests/")
Custom Matchers
bleutest.registerMatcher("beEven", function(received: any): bleutest.MatcherResult
local pass = type(received) == "number" and received % 2 == 0
return {
pass = pass,
message = if pass
then `Expected {received} not to be even`
else `Expected {received} to be even`,
}
end)
expect(4).to.beEven()
CLI Options
The test entry point at tests/init.luau supports these command-line flags:
bleumoon run tests/init -- --reporter tap --filter "my test" --timeout 5
| Flag | Description | Default |
|---|---|---|
--reporter <kind> |
Reporter format: console, tap, or json |
console |
--filter <pattern> |
Only run tests whose name contains the pattern | (none) |
--timeout <seconds> |
Per-test timeout in seconds | 10 |
Development
Prerequisites
Tip: If you have Nix with flakes enabled, both tools are provided automatically via
nix develop.
Setup
git clone https://git.ds.reinitialized.net/bleupigs/bleutest.git
cd bleutest
nix develop # or install BleuMoon and StyLua manually
Self-Tests
bleutest tests itself (dogfooding):
bleumoon run tests/init
Formatting
stylua --check . # check
stylua . # fix
Project Structure
├── .editorconfig # Editor configuration
├── .forgejo/workflows/ # CI/CD pipelines (Forgejo Actions)
├── .luaurc # Luau language settings & aliases
├── bleumoon.toml # Project manifest & dependencies
├── flake.nix # Nix flake for reproducible dev environment
├── stylua.toml # StyLua formatter configuration
├── CHANGELOG.md # Version history
├── LICENSE # MIT License
├── README.md # This file
├── docs/
│ ├── architecture/ # Architecture Decision Records
│ └── investigations/ # Bug investigation write-ups
├── src/
│ ├── init.luau # Library entry point (re-exports public API)
│ ├── types.luau # Shared type definitions
│ ├── matchers.luau # 30+ built-in matcher functions
│ ├── assertions.luau # Fluent expect() API
│ ├── runner.luau # BDD test runner (describe/it/hooks)
│ ├── reporter.luau # Console, TAP, and JSON reporters
│ ├── mock.luau # Mock, spy, and stub utilities
│ └── discovery.luau # Glob-based test file discovery
├── tests/
│ ├── init.luau # Self-test entry point
│ ├── assertions.test.luau
│ ├── runner.test.luau
│ ├── mock.test.luau
│ ├── reporter.test.luau
│ └── discovery.test.luau
├── packages/ # Installed dependencies (auto-managed, gitignored)
└── types/ # Auto-generated type definitions (gitignored)
License
This project is licensed under the MIT License.