Testing#

Lua’s standard library ships assert and no test runner. The operator picks a framework from LuaRocks; the de-facto standard in 2026 is busted, which gives the operator BDD-style describe / it blocks, spies, mocks, and parallel execution. luaunit is the xUnit-flavoured alternative.

/errors`. For the build / lint side of the same toolchain, see Tooling.

busted#

Install with LuaRocks.

$ luarocks install busted
$ busted --version

Specs live under spec/ by default and end in _spec.lua. busted discovers them and runs all matching files.

-- spec/string_spec.lua
describe("string helpers", function()

  it("trims trailing whitespace", function()
    assert.are.equal("hi", trim("hi   "))
  end)

  it("uppercases", function()
    assert.are.equal("HI", string.upper("hi"))
  end)

  describe("when given nil", function()
    it("raises", function()
      assert.has_error(function() trim(nil) end)
    end)
  end)

end)
$ busted spec/
●●●
3 successes / 0 failures / 0 errors / 0 pending : 0.005 seconds

Setup and teardown#

before_each and after_each run around every it; setup and teardown run once per describe.

describe("Server", function()

  local server

  before_each(function()
    server = Server.new(8080)
    server:start()
  end)

  after_each(function()
    server:stop()
  end)

  it("answers ping", function()
    assert.are.equal("pong", server:ping())
  end)

end)

Assertions#

busted ships its own assert (replacing the global) with a fluent API.

Assertion

Meaning

assert.is_true(x)

x == true

assert.is_false(x)

x == false

assert.is_nil(x)

x == nil

assert.is_not_nil(x)

x ~= nil

assert.are.equal(a, b)

a == b by value (use .same for deep table equality)

assert.are.same(a, b)

deep structural equality

assert.has_error(fn, msg)

fn raises (optionally matching msg)

assert.matches(pattern, s)

s matches the Lua pattern

Mocks and spies#

spy wraps a function and records every call; mock wraps an entire table.

it("calls the logger on failure", function()
  local log = mock({warn = function() end})
  run_with_failure(log)
  assert.spy(log.warn).was_called_with("boom")
end)

it("tracks call count", function()
  local s = spy.new(function() end)
  s(); s(); s()
  assert.spy(s).was.called(3)
end)

Coverage#

luacov instruments the runtime and writes luacov.stats.out per run; the luacov command renders it.

$ luarocks install luacov
$ busted --coverage spec/
$ luacov
$ cat luacov.report.out

luaunit#

For projects already on luaunit (xUnit style, no DSL), the operator writes test classes as tables whose methods start with test.

luaunit = require("luaunit")

TestString = {}

function TestString:setUp() self.s = "  hi  " end

function TestString:testTrim()
  luaunit.assertEquals(trim(self.s), "hi")
end

os.exit(luaunit.LuaUnit.run())

Property-based#

lua-quickcheck adds property-based generation for the operator who wants to explore the input space rather than enumerate it.

local qc = require("quickcheck")

qc.property("reverse twice is identity")
  :withtype("string")
  :check(function(s) return rev(rev(s)) == s end)

References#