viernes, 12 de febrero de 2016

Test spies with Lua metatables

Dabbling with Lua metatables, I tried to write a minimal testing library that does not impose you any funny 'describe(...)' or 'it(....)' nesting, and one can just organise the tests as he pleases.

What

I called it spacesuit.lua as it wraps your functions and gives you minimal support to write tests (assertions and spies) in the wild. If you need your tests to be TAP compliant, runnable from any platform, and a well known solution, I can recommend busted, but for me, I tried to keep it minimal so I can put it in my bag and run the files I need from my console, using some silly bash/zsh script using globbing. no need for luarocks, native compilation of lfs or anything.

Apart from providing some sugar for assert_equal (which I'll probably delete in favour of plain assert(foo==42)), it provides:

  • assert_raise(fun): runs the function and asserts an error is thrown during its execution.
  • spy(fun): returns a proxy function (it's a table with __call in its metatble) that logs all the calls (both actual parmeters and results). The usage is quite simple:
  • s = spy(function(x) return x+1 end)
    s(42)
    s(45)
    -- inspect the log
    s.called_with(42) -- true
    s.called_with(42).and_returns_with(43) -- true
    s.called_with(43) -- error
    s.called_with(42).and_returns_with(44) -- error
    
    --number of times called
    s.called() -- true
    s.called(1) -- error
    s.called(2) -- true
    
  • make_spy(Module, 'fun_name'): hijacks Module.fun_name so that you can track executions of functions inside modules. It provides a clean() method that releases the hijacking.
  • stub(Module, 'fun_name', fun): hijacks Module.fun_name and substitutes it for 'fun'.
The whole ungolfed code is (without tests) about 100 lines of lua, which is very impresive for a non-batteries included language.

How


The how is what is interesting. When you make a spy out of a function, spacesuit creates a func table which responds to called_with. called_with  returns a table with and_returns_with key which will do the matching. It's quite a nice usage of lexical scope juggling.

For the hijacking part, I wanted to wrap everything into another table which would have the 'clean' method, and use __call to call the spy table (that would cascade to its __call entry in its metatable, but lua doesn't let you chain __call's. So you have to write the outer one as a function that calls the inner one (and then the __call is run).