A comprehensive test suite for Bleumoon projects
  • Luau 98.1%
  • Nix 1.9%
Find a file
2026-03-19 10:06:49 -05:00
.forgejo/workflows Initial commit 2026-02-12 21:16:45 -06:00
.github release 0.1.1 2026-03-02 21:40:59 -06:00
.vscode release 0.1.1 2026-03-02 21:40:59 -06:00
docs BleuTest Release 0.1.0 2026-02-15 10:35:57 -06:00
src fix init entrypoint 2026-03-18 18:39:29 -05:00
tests release 0.2.0 2026-03-18 18:38:35 -05:00
.editorconfig Initial commit 2026-02-12 21:16:45 -06:00
.gitignore release 0.1.1 2026-03-02 21:40:59 -06:00
.luaurc release 0.2.0 2026-03-18 18:38:35 -05:00
bleumoon.lock update bleumoon.lock 2026-03-18 19:33:45 -05:00
bleumoon.toml update bleumoon.toml 2026-03-19 10:06:49 -05:00
CHANGELOG.md release 0.2.0 2026-03-18 18:38:35 -05:00
flake.lock update bleumoon.toml 2026-03-19 10:06:49 -05:00
flake.nix release 0.2.0 2026-03-18 18:38:35 -05:00
init.luau fix init entrypoint 2026-03-18 18:39:29 -05:00
LICENSE release 0.1.1 2026-03-02 21:40:59 -06:00
README.md release 0.1.1 2026-03-02 21:40:59 -06:00
stylua.toml Initial commit 2026-02-12 21:16:45 -06:00

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 runnerdescribe / it / test with beforeAll, afterAll, beforeEach, afterEach hooks
  • Fluent assertionsexpect(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 & spyingmock.fn(), mock.spy(), mock.stub(), mock.spyOn() with full call tracking
  • Skip & focusit.skip() / describe.skip() and it.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 equality
  • strictEqual(expected) — reference equality (rawequal)

Truthiness

  • beTruthy() — value is truthy (not nil or false)
  • beFalsy() — value is falsy (nil or false)
  • beNil() — value is nil
  • beNotNil() — value is not nil

Type Checks

  • beType(typeName)typeof(value) == typeName
  • beString(), beNumber(), beBoolean(), beTable(), beFunction(), beThread(), beBuffer() — shorthand type matchers

Numeric

  • beCloseTo(expected, tolerance?) — within ±tolerance (default 1e-9)
  • beGreaterThan(n), beGreaterThanOrEqual(n)
  • beLessThan(n), beLessThanOrEqual(n)

String

  • contain(substring) — string contains substring
  • startWith(prefix) — string starts with prefix
  • endWith(suffix) — string ends with suffix
  • match(pattern) — Luau pattern match
  • matchRegex(pattern) — regex match (via @bleumoon/regex)

Table

  • haveLength(n)#value == n
  • haveKey(key) — table contains key
  • haveValue(val) — table contains value (deep equality)
  • containSubset(subset) — table contains all key/value pairs from subset
  • beEmpty() — table has no entries

Error

  • throw() — function throws any error
  • throwMatch(pattern) — function throws error matching a Luau pattern

Mock Verification

  • haveBeenCalled() — mock was called at least once
  • haveBeenCalledTimes(n) — mock was called exactly n times
  • haveBeenCalledWith(...) — 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.