Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking local imports when unit-testing Lua code with Busted

I'm very new to Lua and I'm trying to test a script I have running with an Nginx server. I've been recommended Busted but I can't seem to figure out how to mock some of the local imports.

The Lua code imports the following:

local http = require "resty.http"

And in the test _spec file, I start off like this:

package.path = "files/?.lua;spec/?.lua;" .. package.path

_G.http = require('resty.fake_http')
local app = require('app')

I created a fake_http.lua file inside spec/resty/http.

But when I run a dummy test, I get the following error:

suite spec/app_spec.lua
files/app.lua:3: module 'resty.http' not found:No LuaRocks module found for resty.http

Any Idea what I'm doing wrong here?

like image 950
wonderv Avatar asked Jan 23 '18 20:01

wonderv


1 Answers

There are a couple of small issues preventing your code from working: First, you cannot override the http local by setting a global variable of the same name. The local will always shadow the global variable.

Secondly, require is still being called and if you removed the local in the test code it would still overwrite whatever was held in the global http variable at that time.

What you need is a way to make require load your resty.fake_http module when called as require "resty.http". There are three ways I can think of:

1. Preload the module

The require function uses the two tables package.loaded and package.preload to control when and how modules are loaded (details here). When require is called, it first checks whether package.loaded[module] is set, and if so, returns that value.

This is the first opportunity for mocking the module:

package.loaded["resty.http"] = require "resty.fake_http"
local app = require('app')

Alternatively, if there is no entry in package.loaded, package.preload[module] is checked for a function that can load the module:

package.preload["resty.http"] = function ()
  return require("resty.fake_http")
end
local app = require('app')

2. Change package.path and clobber the module

You are already doing this by adding the spec directory to the path. All you need to do is name your fake module the same as the original, and it will be loaded automatically. e.g. test _spec:

package.path = "files/?.lua;spec/?.lua;" .. package.path
local app = require('app')

In the tested code, it will pick up spec/resty/http.lua automatically.

The difference between these two solutions is that the second one will only require resty.fake_http if the tested code actually requires it, while the first one requires it in any case.

3. Monkeypatch require

This is the ugliest of the three solutions, but it also works fine. require is just another global variable, so you can overwrite it as well:

local _original_require = require
function require(modname, ...)
  if modname == "resty.http" then
    -- implement the exception here
    return _original_require("resty.fake_http")
  end
  -- otherewise act as normal
  return _original_require(modname, ...)
end
local app = require('app')

The second way is the simplest to do and understand, but the first is even cleaner and more versatile. If you can spare the 30 minutes to read the linked documentation and learn how require functions, that can help you in more complex cases for the future.

like image 88
s-ol Avatar answered Oct 05 '22 10:10

s-ol