Test Framework
Test Framework
Section titled “Test Framework”The wippy/test module provides a BDD-style testing framework with assertions, lifecycle hooks, and mocking.
Add the dependency:
wippy add wippy/testwippy installThe module registers a test command automatically. Once installed, wippy run test discovers and runs all test entries in your project.
Defining Tests
Section titled “Defining Tests”Tests are function.lua entries with meta.type: test:
version: "1.0"namespace: app.test
entries: - name: math kind: function.lua meta: type: test suite: math name: Math operations source: file://math_test.lua method: main imports: test: wippy.test:testTest Metadata
Section titled “Test Metadata”| Field | Required | Description |
|---|---|---|
type | Yes | Must be "test" for the runner to discover it |
suite | No | Groups tests in the runner output |
name | No | Display name shown in runner output |
order | No | Sort order within a suite (lower runs first) |
Writing Tests
Section titled “Writing Tests”BDD Style
Section titled “BDD Style”Use describe and it blocks to structure tests:
local test = require("test")
local function define_tests() test.describe("calculator", function() test.it("adds numbers", function() test.eq(1 + 1, 2) end)
test.it("multiplies numbers", function() test.eq(3 * 4, 12) end) end)end
local run_cases = test.run_cases(define_tests)
local function run(options) local result = run_cases(options) if result.failed_tests > 0 then error("tests failed: " .. result.failed_tests) end return resultend
return { run = run }Nested Suites
Section titled “Nested Suites”Suites can be nested for organization:
test.describe("user", function() test.describe("validation", function() test.it("requires name", function() test.ok(validate({}).error) end)
test.it("accepts valid input", function() test.is_nil(validate({name = "Alice"}).error) end) end)
test.describe("formatting", function() test.it("formats display name", function() test.eq(format_name("alice"), "Alice") end) end)end)Skipping Tests
Section titled “Skipping Tests”test.it_skip("not implemented yet", function() test.fail("TODO")end)Skipped tests appear in the output but don’t count as failures.
Suite Aliases
Section titled “Suite Aliases”test.spec and test.context are aliases for test.describe:
test.spec("feature", function() test.context("when valid input", function() test.it("succeeds", function() test.ok(true) end) end)end)Assertions
Section titled “Assertions”Equality
Section titled “Equality”test.eq(actual, expected, msg?) -- actual == expectedtest.neq(actual, expected, msg?) -- actual ~= expectedTruthiness
Section titled “Truthiness”test.ok(val, msg?) -- val is truthytest.fail(msg?) -- unconditional failureNil Checks
Section titled “Nil Checks”test.is_nil(val, msg?) -- val == niltest.not_nil(val, msg?) -- val ~= nilType Checks
Section titled “Type Checks”test.is_true(val, msg?) -- val == truetest.is_false(val, msg?) -- val == falsetest.is_string(val, msg?)test.is_number(val, msg?)test.is_table(val, msg?)test.is_function(val, msg?)test.is_boolean(val, msg?)Strings and Collections
Section titled “Strings and Collections”test.contains(str, substr, msg?) -- substring matchtest.matches(str, pattern, msg?) -- Lua pattern matchtest.has_key(tbl, key, msg?) -- table key existstest.len(val, expected, msg?) -- #val == expectedNumeric Comparisons
Section titled “Numeric Comparisons”test.gt(a, b, msg?) -- a > btest.gte(a, b, msg?) -- a >= btest.lt(a, b, msg?) -- a < btest.lte(a, b, msg?) -- a <= bError Handling
Section titled “Error Handling”test.throws(fn, msg?) -- fn() raises error, returns ittest.has_error(val, err, msg?) -- val is nil, err is not niltest.no_error(val, err, msg?) -- err is nilAll assertions accept an optional message as the last argument. On failure, the message is included in the error output.
Lifecycle Hooks
Section titled “Lifecycle Hooks”test.describe("database", function() test.before_all(function() -- runs once before the suite db = connect() end)
test.after_all(function() -- runs once after the suite db:close() end)
test.before_each(function() -- runs before each test db:begin_transaction() end)
test.after_each(function() -- runs after each test db:rollback() end)
test.it("inserts a record", function() db:exec("INSERT INTO users (name) VALUES ('Alice')") local count = db:query_row("SELECT COUNT(*) FROM users") test.eq(count, 1) end)end)Hooks in nested suites execute in order: parent before_each runs before child before_each, and child after_each runs before parent after_each.
Mocking
Section titled “Mocking”The mock system replaces global object fields and automatically restores them after each test.
Basic Mocking
Section titled “Basic Mocking”test.describe("notifications", function() test.it("sends message", function() local sent = false test.mock("process.send", function(pid, topic, payload) sent = true end)
notify_user("hello") test.is_true(sent) -- mock is auto-restored after this test end)end)Mock API
Section titled “Mock API”test.mock("object.field", replacement) -- replace a global fieldtest.mock_process("field", replacement) -- shorthand for process fieldstest.restore_mock("object.field") -- restore one mocktest.restore_all_mocks() -- restore all mocksMock paths use dot notation: "process.send" replaces _G.process.send.
Mocks for process.send automatically proxy test framework messages through the original function, so test event reporting continues to work when process.send is mocked.
All mocks are automatically restored after each test via the after_each hook.
Running Tests
Section titled “Running Tests”Run All Tests
Section titled “Run All Tests”wippy run testFilter by Pattern
Section titled “Filter by Pattern”wippy run test mathwippy run test user validationFilters match against entry IDs. Multiple patterns are combined.
Example Output
Section titled “Example Output”3 tests in 1 suites
calculator + adds numbers 0ms + multiplies numbers 0ms - divides by zero 1ms Error: expected error, got nil
1 suite | 2 passed | 1 failed | 0 skipped | 3msSimple Tests
Section titled “Simple Tests”For tests that don’t need the BDD framework, define a simple function that returns true or raises an error:
local funcs = require("funcs")
local function main() local result, err = funcs.call("app:my_function", "input") if err then error("call failed: " .. tostring(err)) end if result ~= "expected" then error("expected 'expected', got: " .. tostring(result)) end return trueend
return { main = main } - name: integration kind: function.lua meta: type: test suite: integration source: file://integration_test.lua method: main modules: - funcsThe runner detects whether a test uses BDD case events or returns a simple value. Both patterns work with wippy run test.
Project Structure
Section titled “Project Structure”A typical test layout:
src/ _index.yaml app.lua test/ _index.yaml # test entries math_test.lua user_test.lua integration_test.luaThe test _index.yaml defines the test namespace and entries:
version: "1.0"namespace: app.test
entries: - name: math kind: function.lua meta: type: test suite: math source: file://math_test.lua method: main imports: test: wippy.test:test
- name: user kind: function.lua meta: type: test suite: user source: file://user_test.lua method: main imports: test: wippy.test:testInfrastructure Requirements
Section titled “Infrastructure Requirements”The test runner needs a process.host and terminal.host in your application. These are typically already present. If not, add them:
entries: - name: processes kind: process.host lifecycle: auto_start: true
- name: terminal kind: terminal.host lifecycle: auto_start: trueSee Also
Section titled “See Also”- Framework Overview - Framework module usage
- CLI Reference - CLI commands
- Functions - Function registry