1

I am trying to unit test code using Busted (which I did not write, nor am I allowed to refactor at this time for business reasons) in Lua, and there is no concept in this module of classes nor dependency injection. So, I'd like to replace some of the modules required at the top of the file, i.e local log = require("path.to.module.logger"):new() with a mocked logger that I made to track the number of times methods are called, i.e logger:trace() such as with times() in Mockito in Java. In Java, I can use Reflection.Utils for this purpose. What's the equivalent in Lua to help make this not-testable code, testable?

I've already tried creating a global variable with the same variable name log and setting it equal to my mock using this example: https://www.lua.org/pil/14.2.html

local _M = {}

local log = require("path.to.module.logger"):new()

...

function _M.init(...) log:trace("debug") # I would like this log instance to not be the one above, rather the one I inject into the module at runtime end

Quinn Vissak
  • 131
  • 1
  • 8

3 Answers3

1

"Reflection" is not really a thing in Lua, not in the Java sense of that term. As a language that uses Duck Typing, everything is very open. Lua only has one data structure: a table. Everything in Lua comes from tables. A module is just a table returned by the chunk loaded by require.

The contents and data structures behind a table can be hidden via metatables, which can be used to prevent the normal iteration process (pairs, ipairs, and so forth) for accessing elements in a table. However, you can always use getmetatable to extract the metatable itself and call it; you can even break through the usual way of hiding a metatable with debug.getmetatable.

That being said, because Lua relies on Duck Typing, and because Lua's APIs are pretty open, it's rather difficult to be comprehensive about wrapping every single function and table.

For example, let's say you want to wrap a module. That's easy enough; just create an empty table which has a metatable whose metamethods call the wrapped module methods. That works for the direct APIs of the module.

But what happens if one of those APIs returns an object which itself needs to be wrapped? How will you be able to tell the difference between a specialized API object and just a regular table? Equally importantly, if you can successfully identify which return values need to be wrapped, how do you do that? After all, if they pass one of your wrapper tables to a wrapper API function, it now needs to unwrap that table so that it can pass the wrapped table to the actual function being wrapped.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
0

I was actually able to find an answer this morning from a colleague. "Reflection" is not really possible, as Nicol Bolas suggested in his answer, however, from the lua documentation (http://lua-users.org/wiki/ModulesTutorial), we learn that:

Lua caches modules in the package.loaded table.

That means we can overwrite the package.loaded table in our busted test and essentially, at runtime, replace dependencies in tightly coupled code (much like mocking in Mockito via dependency injection in Java). For example:

package.loaded["path.to.module.logger"] = my_logger would replace the dependency path.to.module.logger globally with my_logger assuming it adhered to the same contract.

Quinn Vissak
  • 131
  • 1
  • 8
0

I would write my mock for the logger and set it under same path as the original logger, but under different directory root. Then for the tests add folder with mocks at the beginning of the LUA_PATH

Example : /tmp/a/package/logger.lua :

local _M = {}

_M.log = function()
  print "Original logger"
end

return _M

/tmp/b/package/logger.lua:

local _M = {}

_M.log = function()
  print "Mocked logger"
end

return _M

and the test /tmp/test/logger_spec.lua:

describe("Test suite", function()
  it("Testing the mock", function ()

    local log = require("package.logger")

    log.log()
  end)
end)

if you set LUA_PATH to use original : export LUA_PATH="/tmp/a/?.lua;;" and call busted :

busted logger_spec.lua
Original logger
●
1 success / 0 failures / 0 errors / 0 pending : 0.000527 seconds

and now point the LUA_PATH to your mock : export LUA_PATH="/tmp/b/?.lua;;"

and call busted again

busted logger_spec.lua
Mocked logger
●
1 success / 0 failures / 0 errors / 0 pending : 0.000519 seconds