Test Framework
The wippy/test module provides BDD suites, assertions, lifecycle hooks, mocks, and a runner for test entries.
This page is an API primer. Its Lua, YAML, output, and project-layout blocks are reference snippets that can be combined in an existing Wippy project; they are not one copy-and-run project. Names such as validate, format_name, db, connect, and notify_user stand for application functions or modules supplied by the test subject. For a complete runnable example, follow Testing a Wippy Application.
Setup
Add the dependency:
wippy add wippy/test
wippy install
The module registers the test entrypoint automatically (a command with use_case: test). Once installed, wippy test discovers and runs all test entries in your project.
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:test
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
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 result
end
return { run = run }
Nested Suites
Suites can be nested to group related behavior:
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
test.it_skip("not implemented yet", function()
test.fail("TODO")
end)
Skipped tests appear in the output but do not count as failures.
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
Equality
test.eq(actual, expected, msg?) -- actual == expected
test.neq(actual, expected, msg?) -- actual ~= expected
Truthiness
test.ok(val, msg?) -- val is truthy
test.fail(msg?) -- unconditional failure
Nil Checks
test.is_nil(val, msg?) -- val == nil
test.not_nil(val, msg?) -- val ~= nil
Type Checks
test.is_true(val, msg?) -- val == true
test.is_false(val, msg?) -- val == false
test.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
test.contains(str, substr, msg?) -- substring match
test.matches(str, pattern, msg?) -- Lua pattern match
test.has_key(tbl, key, msg?) -- table key exists
test.len(val, expected, msg?) -- #val == expected
Numeric Comparisons
test.gt(a, b, msg?) -- a > b
test.gte(a, b, msg?) -- a >= b
test.lt(a, b, msg?) -- a < b
test.lte(a, b, msg?) -- a <= b
Error Handling
test.throws(fn, msg?) -- fn() raises error, returns it
test.has_error(val, err, msg?) -- val is nil, err is not nil
test.no_error(val, err, msg?) -- err is nil
All assertions accept an optional message as the last argument. On failure, the message is included in the error output.
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
The mock system replaces global object fields and restores them after each test.
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
test.mock("object.field", replacement) -- replace a global field
test.mock_process("field", replacement) -- shorthand for process fields
test.restore_mock("object.field") -- restore one mock
test.restore_all_mocks() -- restore all mocks
Mock 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
Run All Tests
wippy test
Filter by Pattern
wippy test math
wippy test user validation
Filters match literal substrings of entry IDs. When you provide multiple patterns, an entry runs if its ID matches any of them.
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 | 3ms
Simple Tests
For tests that do not need BDD suites, define a 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 true
end
return { main = main }
- name: integration
kind: function.lua
meta:
type: test
suite: integration
source: file://integration_test.lua
method: main
modules:
- funcs
The runner detects whether a test uses BDD case events or returns a simple value. Both patterns work with wippy test.
Project Structure
A typical test layout:
src/
_index.yaml
app.lua
test/
_index.yaml # test entries
math_test.lua
user_test.lua
integration_test.lua
The 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:test
Terminal Host
wippy/test depends on wippy/terminal, which supplies the auto-starting
wippy.terminal:host used by the CLI runner. Applications do not need to
declare a separate process or terminal host solely to run wippy test.
See Also
- Framework Overview — Install and import framework modules
- CLI Reference — Test command and flags
- Functions — Function entries and invocation