A Lua testing framework in 31 lines
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
endreturn 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.