Files
metabuilder/packages/lua_test
2025-12-30 17:41:10 +00:00
..

Lua Test Package

A unit testing framework for Lua scripts in MetaBuilder packages.

Features

  • BDD-style syntax: describe/it blocks for organizing tests
  • Rich assertions: expect() with chainable matchers
  • Mocks and spies: Track function calls and replace implementations
  • Lifecycle hooks: beforeAll, afterAll, beforeEach, afterEach
  • Test filtering: Run specific tests with .only or name filters
  • Detailed reporting: Text and JSON output formats

Package Integration

To use lua_test in your package, add it as a devDependency and declare your test files in metadata.json:

{
  "packageId": "my_package",
  "name": "My Package",
  "version": "1.0.0",
  "dependencies": ["ui_permissions"],
  "devDependencies": ["lua_test"],
  "exports": {
    "scripts": ["my_script"]
  },
  "tests": {
    "scripts": [
      "tests/my_script.test.lua"
    ]
  }
}

Place your test files in seed/scripts/tests/:

packages/my_package/
├── seed/
│   ├── metadata.json
│   └── scripts/
│       ├── my_script.lua
│       └── tests/
│           └── my_script.test.lua

Quick Start

local framework = require("lua_test/framework")
local assertions = require("lua_test/assertions")

local describe = framework.describe
local it = framework.it
local expect = assertions.expect

describe("My Module", function()
  
  it("should do something", function()
    expect(1 + 1).toBe(2)
  end)
  
  it("should handle strings", function()
    expect("hello").toContain("ell")
    expect("hello").toHaveLength(5)
  end)
  
end)

Assertions

Basic Matchers

expect(value).toBe(expected)           -- Strict equality (==)
expect(value).toEqual(expected)        -- Deep equality for tables
expect(value).toBeNil()                -- Check for nil
expect(value).toBeTruthy()             -- Truthy check
expect(value).toBeFalsy()              -- Falsy check
expect(value).toBeType("string")       -- Type check

Comparison Matchers

expect(num).toBeGreaterThan(5)
expect(num).toBeLessThan(10)
expect(3.14159).toBeCloseTo(3.14, 2)   -- Floating point comparison

String Matchers

expect(str).toContain("substring")
expect(str).toMatch("^pattern")        -- Lua pattern matching
expect(str).toHaveLength(5)

Collection Matchers

expect(table).toContain(element)
expect(table).toHaveLength(3)
expect(table).toHaveProperty("key")
expect(table).toHaveProperty("key", value)

Error Matchers

expect(function() error("boom") end).toThrow()
expect(function() error("boom") end).toThrow("boom")

Negation

Use .never to negate any matcher:

expect(5).never.toBe(10)
expect("hello").never.toContain("world")

Mocks

Mock Functions

local mocks = require("lua_test/mocks")

local mockFn = mocks.fn(function(x) return x * 2 end)

mockFn(5)
mockFn(10)

mockFn.getCallCount()     -- 2
mockFn.wasCalled()        -- true
mockFn.wasCalledWith(5)   -- true
mockFn.getLastCall()      -- {10}
mockFn.getCalls()         -- {{5}, {10}}

Mock Return Values

local mockFn = mocks.fn()
mockFn.mockReturnValue(42)

mockFn()  -- 42
mockFn()  -- 42

-- Or return different values
mockFn.mockReturnValueOnce(1)
mockFn.mockReturnValueOnce(2)
mockFn()  -- 1
mockFn()  -- 2
mockFn()  -- nil (or previous mockReturnValue)

Spies

local myModule = { calculate = function(x) return x * 2 end }

local spy = mocks.spyOn(myModule, "calculate")

myModule.calculate(5)

spy.getCallCount()     -- 1
spy.wasCalledWith(5)   -- true

spy.mockRestore()      -- Restore original function

Lifecycle Hooks

describe("My Suite", function()
  
  beforeAll(function()
    -- Run once before all tests in this suite
  end)
  
  afterAll(function()
    -- Run once after all tests in this suite
  end)
  
  beforeEach(function()
    -- Run before each test
  end)
  
  afterEach(function()
    -- Run after each test
  end)
  
  it("test 1", function() end)
  it("test 2", function() end)
  
end)

Test Filtering

Skip Tests

xit("this test is skipped", function()
  -- Won't run
end)

Focus Tests

fit("only this test runs", function()
  -- Other tests in the suite are skipped
end)

Filter by Name

framework.configure({ filter = "validation" })
-- Only tests with "validation" in their name will run

Running Tests

local runner = require("lua_test/runner")
local framework = require("lua_test/framework")

-- Define your tests with describe/it
-- ...

-- Run all suites
local results = runner.runAll(framework.getSuites())

-- Print report
print(runner.formatReport(results))

-- Or get JSON for programmatic use
local json = runner.formatJSON(results)

Helpers

local helpers = require("lua_test/helpers")

-- Generate test data
local users = helpers.generateTestData({
  id = "$index",
  name = function(i) return "User " .. i end,
  score = "$random"
}, 10)

-- Table utilities
helpers.table.clone(t)
helpers.table.merge(t1, t2)
helpers.table.keys(t)
helpers.table.values(t)
helpers.table.size(t)

-- String utilities
helpers.string.trim(s)
helpers.string.split(s, delimiter)
helpers.string.startsWith(s, prefix)
helpers.string.endsWith(s, suffix)

-- Create test context
local ctx = helpers.createContext({ user = nil })
ctx.set("user", { name = "Alice" })
ctx.get("user")
ctx.reset()

Example: Testing a Package

-- tests/validate.test.lua
local framework = require("lua_test/framework")
local assertions = require("lua_test/assertions")
local runner = require("lua_test/runner")

local describe = framework.describe
local it = framework.it
local beforeEach = framework.beforeEach
local expect = assertions.expect

-- Import the module to test
local validate = require("ui_login/validate")

describe("ui_login validation", function()
  
  describe("login", function()
    it("should reject empty username", function()
      local result = validate.login({ username = "", password = "password123" })
      expect(result.valid).toBe(false)
      expect(result.errors[1].field).toBe("username")
    end)
    
    it("should accept valid credentials", function()
      local result = validate.login({ username = "user", password = "password123" })
      expect(result.valid).toBe(true)
    end)
  end)
  
end)

-- Run and report
local results = runner.runAll(framework.getSuites())
print(runner.formatReport(results))

Output Example

═══════════════════════════════════════
          TEST RESULTS REPORT          
═══════════════════════════════════════

📦 ui_login validation
  📦 login
    ✅ should reject empty username (0.12ms)
    ✅ should accept valid credentials (0.08ms)
  📦 register
    ✅ should reject invalid email (0.15ms)
    ✅ should accept valid registration (0.10ms)

───────────────────────────────────────
Summary:
  Total:   4 tests
  Passed:  4 ✅
  Failed:  0 ❌
  Skipped: 0 ⏭️
  Duration: 0.45ms

🎉 All tests passed!
═══════════════════════════════════════