Test Api

Browse docs

The lde-test framework provides a minimal API for defining and running tests.

It ships with the lde runtime and is injected into every test file that lde test runs, so you get the framework by requiring lde-test:

local test = require("lde-test")

Typings

To get the LuaCATs types for lde-test, add the lde-test package as a dev dependency to your package:

lde add --dev gh:lde-test@lde-org/lde

Registering Tests

test.it(name: string, fn: fun())

Registers a test with the given name and function. fn is executed when the suite runs; an assertion that fails throws out of it and marks the test as failed.

test.it("writes a file", function()
	fs.write("tmp/test-output", "hello")
	test.truthy(fs.exists("tmp/test-output"))
end)

test.skip(name: string, fn: fun()?)

Registers a test that is reported as skipped and never executed. Use it for tests you know are broken or not yet implemented:

test.skip("not implemented yet")

test.skipIf(condition: boolean) -> fun(name: string, fn: fun())

Returns a function that registers a test just like test.it, except the test is skipped when condition is truthy. Handy for gating tests on the platform or environment:

local isWindows = package.config:sub(1, 1) == "\\"

test.skipIf(isWindows)("unix-only behavior", function()
	-- ...
end)

Hooks

test.afterEach(fn: fun())

Registers fn to run after every executed test, whether it passed or failed. If it throws, the test it follows is reported as failed:

local scratch = {}

test.it("adds an entry", function()
	scratch.key = "value"
	test.equal(scratch.key, "value")
end)

test.afterEach(function()
	scratch = {}
end)

test.afterAll(fn: fun())

Registers fn to run once, after all registered tests have run. Use it for teardown that should happen exactly once no matter how many tests ran:

test.afterAll(function()
	-- shared teardown
end)

Assertions

Assertions throw when their check fails, which fails the test they run inside.

test.equal(a: any, b: any, msg?: string)

Fails unless a equals b. Comparison follows Lua semantics: numbers and strings compare by value, tables by identity, so use test.deepEqual when you care about table contents.

test.equal(1 + 1, 2)
test.equal("foo", "foo")

test.notEqual(a: any, b: any, msg?: string)

Fails when a equals b:

test.notEqual(1, 2)

test.truthy(value: any, msg?: string)

Fails when value is false or nil:

test.truthy(fs.exists("src/init.lua"))

test.falsy(value: any, msg?: string)

Fails when value is anything other than false or nil:

test.falsy(fs.exists("tmp/scratch"))

test.includes(haystack: string, needle: string, msg?: string)

Fails when needle is not a substring of haystack. This is a plain substring search, so Lua patterns are not interpreted:

test.includes("hello world", "world")

test.greater(a: number, b: number, msg?: string)

Fails unless a is greater than b:

test.greater(#items, 0)

test.less(a: number, b: number, msg?: string)

Fails unless a is less than b:

test.less(processed, total)

test.greaterEqual(a: number, b: number, msg?: string)

Fails unless a is greater than or equal to b:

test.greaterEqual(score, 100)

test.lessEqual(a: number, b: number, msg?: string)

Fails unless a is less than or equal to b:

test.lessEqual(elapsed, timeout)

test.deepEqual(a: any, b: any, msg?: string)

Recursively compares a to b. Fails on any difference: mismatched values at any depth, keys present in a but missing from b, type mismatches, or different metatables. Array order matters, since indices are compared as keys.

local got = { name = "alice", tags = { "admin" } }
test.deepEqual(got, { name = "alice", tags = { "admin" } })

test.match(actual: table, expected: table, msg?: string)

Fails unless actual contains every key/value pair in expected. Values in expected are compared recursively, and extra keys in actual are fine:

local response = { status = 200, body = { ok = true, id = 42 } }
test.match(response, { status = 200 })
test.match(response, { body = { ok = true } })

test.errors(fn: fun(), expected?: any, msg?: string)

Fails unless fn throws. With expected, the thrown error must match it: string errors are compared without the path:line: prefix that pcall adds, and non-string errors are compared by identity.

test.errors(function() error("boom") end)
test.errors(function() error("boom") end, "boom")

local err = { code = 42 }
test.errors(function() error(err) end, err)

Helpers

test.count(tbl: table) -> number

Returns the number of keys in tbl, counting array and hash entries alike:

test.equal(test.count({ "a", "b", "c" }), 3)
test.equal(test.count({ x = 1, y = 2 }), 2)