A Lua testing framework in 31 lines

Jasper Lyons
4 min readFeb 22, 2018

30 lines would have been snazzier but I like blank line separators.

So, I’m working on a web framework in Lua (yes there are loads but this is a learning exercise). While writing my string templating library I discovered some issues that made me wish I had unit tests. I looked at the existing Lua test frameworks but since this is a learning exercise and I am feeling allergic to dependencies, I thought Id write my own.

Turns out it only takes ~ 1 hour to do! I’ll pop the code below and walk through it section by section and then show you how I’ve used it and what it prints out. Sound good? Cool.

Lets break this down.

local function describe(name, descriptor)
local errors = {}
local successes = {}

Here we’re defining a local function called describe that takes a name and a descriptor . name is just a string that identifies the suite of tests, in my case something like "Parser" . descriptor is a function that the end user will write their tests in! You’ll get to see how this is used later.

  function it(spec_line, spec)
local status = xpcall(spec, function (err)
table.insert(
errors,
string.format("\t%s\n\t\t%s\n", spec_line, err)
)
end)

Next we define another function inside of describe called it . it is hidden to the outside world which means that only things inside of describe can use it. We’re going to use it to allow me to write my individual unit tests.

it takes a spec_line and a spec . spec_line is just a string that identified the unit test, I might use something like "should return total new lines in a file" as the spec_line. spec is a function within which our unit test will be written!

Lets focus on the next part a little. xpcall is a function that takes another function and runs it in “protected mode”. This means that errors that are thrown inside of the function do not kill the program, instead they are passed into a message handler. You see the anonymous function above, the second parameter to xpcall ? That’s our error handler. Inside of that, we capture any errors and table.insert them into our errors table. This means we can capture many errors over a run and print them all out at the end instead of falling over at the first one.

I’m passing xpcall the spec function to run which means any errors that are thrown in the unit tests are caught and saved in the errors table.

    if status then
table.insert(successes, string.format("\t%s\n", spec_line))
end
end

xpcall returns a “status” that indicates if the function ran without any errors. If it does run without any errors I’m assuming that it was a successful run! So we table.insert it into the successes table. Then we can print all of those out later too.

local status = xpcall(descriptor, function (err)
table.insert(errors, err)
end, it)

So we’re out of the it method now and we’re using xpcall again? This is so that we can catch any errors that occur while setting up our test suite in the descriptor function. We’ll just add that to the list of errors for now but it might be better to just fail everything if the descriptor fails.

By the way, we’re passing in the it function to the descriptor at the end there. That means the user (I) can access the it function from the first argument passing to my descriptor function. You’ll see this in a bit.

  print(name)
if #errors > 0 then
print('Failures:')
print(table.concat(errors))
end
if #successes > 0 then
print('Successes:')
print(table.concat(successes))
end
end
return describe

Finally we’re printing out the errors and successes. We start with the name of the test suite (or descriptor, as I’ve been calling it for some reason). Then we check, “are there any errors?” — if yes print them out! Then we do the same for successes. It ends up looking like this:

Test function
Failures:
should print out this failure
lspec/spec.lua:5: 1 should equal 2!
Successes:
should print out this success

Anyone familiar with unit testing frameworks (me, the end users here) should be able to figure out what this means. “Test function” is the descriptor name, “Failures:” is all the things that went wrong and were they went wrong and “Successes:” is all the things that went right!

So, here’s how you write tests with this:

local describe = require('lspec')describe('Test function', function (it)
it('should print out this failure', function ()
assert(1 == 2, '1 should equal 2!')
end)
it('should print out this failure', function ()
assert(1 == 2, '1 should equal 2!')
end)
it('should print out this success', function ()
-- test that do nothing succeed
end)
end)

Yay, rpsec (sort of) in Lua.

Off to write some unit tests and figure out why this parser keeps breaking.

--

--

Jasper Lyons

Senior Teaching Fellow @ RHUL (prev: CTO @ Release Platform, CPO @ Kick Advisor, iOS Dev @ LASU)