Lua Testing
SpyWeb includes a lightweight Lua testing mode for job hooks. Tests are kept close to production logic while running in fresh, isolated Lua VMs.
CLI Usage
Section titled “CLI Usage”spyweb testspyweb test "Job Name"spyweb test "Job Name" pricespyweb test- runs everytest_*function across all jobsspyweb test "Job Name"- runs tests for the matching jobspyweb test "Job Name" price- runs tests whose name contains “price”
Test Discovery
Section titled “Test Discovery”SpyWeb loads hooks.lua, defer.lua, and tests.lua from the job directory, then scans for global functions starting with test_.
function extract_id(text) return text:match("ID%-(%d+)")end
function test_extract_id() spyweb.assert_eq(extract_id("Product ID-9982"), "9982")endExecution Model
Section titled “Execution Model”Each test function runs in its own fresh Lua VM:
- Global mutations don’t leak between tests
- Each test gets a new temporary database (backend depends on build feature flags)
defer.luais loaded alongsidehooks.luaandtests.lua, so tests see the same lifecycle helpers as production- Production Lua bindings are available (HTTP bindings are async under the hood - the test runner executes them asynchronously)
- Production
ctxis registered asactive_ctx
Available Bindings
Section titled “Available Bindings”http_get,http_post,http_request,http_multipartfs_read,fs_read_binary,store_*,global_store_*spyweb.assert_eq(left, right, [message])spyweb.assert_ne(left, right, [message])
Example: HTTP Test
Section titled “Example: HTTP Test”function test_fetch_remote_page() local res = http_get("http://127.0.0.1:8080/") spyweb.assert_eq(res.status, 200) spyweb.assert_eq(res.body, "OK")end
function test_head_request() local res = http_request({ method = "HEAD", url = "http://127.0.0.1:8080/" }) spyweb.assert_eq(res.status, 200) spyweb.assert_eq(res.body, "")endExample: Storage Seed
Section titled “Example: Storage Seed”function test_seed_state() store_set("page", "3") spyweb.assert_eq(store_get("page"), "3")endMocking
Section titled “Mocking”Tests can overwrite globals inside the Lua VM:
function test_override_fetch_logic() override_fetch = function(req, ctx) return { status = 200, body = "mock", url = req.url } end local result = override_fetch({ url = "https://example.com" }) spyweb.assert_eq(result.body, "mock")endAssert Helpers
Section titled “Assert Helpers”spyweb.assert_eq(left, right, [message])- passes ifleft == right. The optionalmessageis included in failure output.spyweb.assert_ne(left, right, [message])- passes ifleft ~= right.
Practical Limits
Section titled “Practical Limits”- Name-based discovery - matches by job name or normalized job id
- Only globals -
localfunctions are not visible to the test runner - Isolated VMs - each test runs in its own VM; globals do not leak between tests
- defer lifecycle -
defer()works as in production; registered cleanup runs when the test finishes
Recommended Structure
Section titled “Recommended Structure”For small jobs - keep tests in hooks.lua next to the hook they verify.
For larger jobs:
Directoryproject/
Directoryjobs/
Directoryinventory-sync/
- hooks.lua Production hooks
- defer.lua Lifecycle helpers
- tests.lua Shared helpers and broader test cases
Example Suite
Section titled “Example Suite”hooks.lua - production hook and helper:
function extract_id(text) return text:match("ID%-(%d+)")end
function override_fetch(req, ctx) -- production logicendtests.lua - shared helper and test cases:
-- shared helperfunction make_test_request(url) return http_request({ method = "GET", url = url })end
function test_extract_id() spyweb.assert_eq(extract_id("ID-001"), "001")end
function test_fetch_reachable() local res = make_test_request("http://127.0.0.1:8080/") spyweb.assert_eq(res.status, 200)endFailure Output
Section titled “Failure Output”running 1 test for job 'inventory-sync'test test_id_extraction ... FAILED
failures:
---- test_id_extraction stdout ----...
test result: FAILED. 0 passed; 1 failed; finished in 0.01sTroubleshooting
Section titled “Troubleshooting”No tests found: Ensure function name starts with test_, is global (not local), and is in hooks.lua, defer.lua, or tests.lua.
HTTP test fails: Confirm URL is reachable. Each test runs in a fresh VM.
A helper defined in defer.lua is missing: Verify the filename is defer.lua (not defer.luau) and it is placed next to hooks.lua.