Skip to content
AgileLogics
6 min read

Testing and Debugging

Prerequisites: Error handling, Modules and require

Testing with busted

busted is the most popular Lua testing framework:

-- illustrative
-- spec/pricing_spec.lua
local pricing = require("pricing")

describe("pricing.discount", function()
  it("gives no discount under 100", function()
    assert.are.equal(0, pricing.discount(50))
  end)

  it("gives 10% at 100 or more", function()
    assert.are.equal(20, pricing.discount(200))
  end)

  it("rejects negative totals", function()
    assert.has_error(function() pricing.discount(-1) end, "total cannot be negative")
  end)
end)
$ busted
+++
3 successes / 0 failures / 0 errors / 0 pending : 0.004 seconds

describe groups tests, it defines one, and assert.are.same compares tables deeply. Use luassert spies and stubs to replace functions during tests.

A tiny test runner

Where you can't install busted — inside a game or another host — a few lines of plain Lua are enough:

local passed, failed = 0, 0
local function test(name, fn)
  local ok, err = pcall(fn)
  if ok then passed = passed + 1
  else failed = failed + 1; print("FAIL " .. name .. ": " .. tostring(err)) end
end
local function eq(a, b) if a ~= b then error(tostring(a) .. " ~= " .. tostring(b), 2) end end

local function discount(total)
  if total < 0 then error("negative total") end
  return total >= 100 and total * 0.1 or 0
end

test("no discount under 100", function() eq(discount(50), 0) end)
test("10% at 100", function() eq(discount(200), 20.0) end)
test("deliberately wrong", function() eq(discount(100), 5) end)
test("negative raises", function() assert(not pcall(discount, -1)) end)
print(passed .. " passed, " .. failed .. " failed")
FAIL deliberately wrong: main.lua:16: 10.0 ~= 5
3 passed, 1 failed

The debug library

local function whereAmI()
  local info = debug.getinfo(2, "Sln")
  return string.format("%s:%d in %s", info.short_src, info.currentline, info.name or "?")
end

local function process()
  print(whereAmI())
end
process()

local function inspectLocals()
  local a, b = 1, "two"
  local i = 1
  while true do
    local name, value = debug.getlocal(1, i)
    if not name then break end
    if name ~= "i" then print(name, value) end
    i = i + 1
  end
end
inspectLocals()
main.lua:7 in process
a	1
b	two

debug.getinfo describes a function or stack level; debug.getlocal reads local variables; debug.traceback builds a stack trace; debug.sethook runs code on every call, line, or N instructions — the basis of debuggers and profilers.

Debuggers

  • VS Code — the "Lua Debug" extension (by actboy168) supports Lua 5.1–5.4 and LuaJIT, with breakpoints and variable inspection.
  • ZeroBrane Studio — built-in debugger, including remote debugging of LÖVE and other hosts.
  • In Neovim, nvim-dap with one-small-step-for-vimkind debugs Neovim Lua itself.