Lua Test Library Design
Minimal test framework for normalize scripts.
Goals
- Simple test organization with pass/fail tracking
- Rich assertion library
- Property-based testing via
type.generateintegration - No external dependencies
Module Structure
test.lua -- Core test runner and assertions
test/property.lua -- Property-based testingBasic Usage
Running Tests
local test = require("test")
test.test("addition works", function()
test.assert.equals(2 + 2, 4)
end)
test.test("strings concatenate", function()
test.assert.equals("hello" .. " world", "hello world")
end)
test.report() -- prints summary, returns true if all passedOutput:
All 2 tests passedTest Lifecycle
test.reset() -- clear pass/fail counts (optional, for multiple test runs)
-- run tests...
test.report() -- print summary and return success booleanAssertions
All assertions take an optional msg parameter for custom error messages.
Equality
test.assert.equals(a, b, msg) -- a == b (shallow)
test.assert.same(a, b, msg) -- deep equality for tablesTruthiness
test.assert.is_true(v, msg) -- v is truthy
test.assert.is_false(v, msg) -- v is falsy
test.assert.is_nil(v, msg) -- v == nil
test.assert.is_not_nil(v, msg) -- v ~= nilTypes
test.assert.is_type(v, "string", msg) -- type(v) == expectedStrings
test.assert.contains(str, substr, msg) -- string.find(str, substr)
test.assert.matches(str, pattern, msg) -- string.match(str, pattern)Collections
test.assert.is_in(value, tbl, msg) -- value exists in tableErrors
test.assert.throws(fn, pattern, msg) -- fn() errors, message matches pattern
test.assert.does_not_throw(fn, msg) -- fn() succeedsNumeric Comparisons
test.assert.near(a, b, tolerance, msg) -- |a - b| <= tolerance
test.assert.gt(a, b, msg) -- a > b
test.assert.gte(a, b, msg) -- a >= b
test.assert.lt(a, b, msg) -- a < b
test.assert.lte(a, b, msg) -- a <= bProperty-Based Testing
The test.property module generates random values from type schemas and verifies properties hold for all of them.
Basic Property Check
local property = require("test.property")
local T = require("type")
-- Check that string length is non-negative
local ok, err = property.check(
T.string({ min_len = 0, max_len = 100 }),
function(s)
assert(#s >= 0)
end
)
if not ok then
print("Property failed: " .. err)
endIntegration with Test Module
Use property.prop() to create a test function:
local test = require("test")
local property = require("test.property")
local T = require("type")
test.test("integers are numbers", property.prop(
"integer check",
T.integer({ min = -100, max = 100 }),
function(n)
assert(type(n) == "number")
end
))
test.report()Immediate Assertion
Use property.assert() for inline property checks:
local property = require("test.property")
local T = require("type")
property.assert(
T.array(T.integer()),
function(arr)
-- array length is non-negative
assert(#arr >= 0)
end,
{ iterations = 50 }
)Options
property.check(schema, fn, {
iterations = 100, -- number of random values to test (default: 100)
seed = 12345, -- random seed for reproducibility
})Struct Properties
local user_schema = T.struct({
name = T.string({ min_len = 1, max_len = 50 }),
age = T.integer({ min = 0, max = 150 }),
})
property.assert(user_schema, function(user)
assert(#user.name >= 1)
assert(user.age >= 0)
end)Failure Reporting
When a property fails, the error message includes:
- Which iteration failed
- The generated value that caused failure
- The original error message
Property failed on iteration 42 with value: { name = "x", age = -5 }
Error: assertion failed: age must be non-negativeFull Example
local test = require("test")
local property = require("test.property")
local T = require("type")
-- Unit tests
test.test("empty table has zero length", function()
test.assert.equals(#{}, 0)
end)
test.test("table insert increases length", function()
local t = {1, 2, 3}
table.insert(t, 4)
test.assert.equals(#t, 4)
end)
-- Property tests
test.test("string reversal is involutory", property.prop(
"reverse twice equals original",
T.string({ min_len = 0, max_len = 20 }),
function(s)
local reversed = s:reverse():reverse()
assert(reversed == s)
end,
{ iterations = 50 }
))
test.test("array concat preserves elements", property.prop(
"concat length",
T.struct({
a = T.array(T.integer()),
b = T.array(T.integer()),
}),
function(input)
local combined = {}
for _, v in ipairs(input.a) do table.insert(combined, v) end
for _, v in ipairs(input.b) do table.insert(combined, v) end
assert(#combined == #input.a + #input.b)
end
))
-- Report results
local success = test.report()
os.exit(success and 0 or 1)Design Decisions
No test discovery: Tests run inline as the file executes. Simple and predictable.
Assertions on
test.assert: Namespaced to avoid polluting globalassert. The globalassertstill works for quick checks.Property testing uses type schemas: Leverages
type.generatefor random value generation. No separate shrinking—failed cases show the exact value.No beforeEach/afterEach: Keep it simple. Use local functions if setup is needed.
Exit code convention:
test.report()returns boolean for use withos.exit().