James Butherway explains the reasons for testing Nginx functionality with Lua unit tests.

Testing Nginx functionality with Lua unit tests

James Butherway -  02 Oct, 2015

In today's software engineering blog, James Butherway takes you through the whys and wherefores of testing Nginx functionality with Lua unit tests.

If you are running an Nignx server and have found the vanilla directives didn't give you enough, you may have plugged in the HttpLuaModule. If you haven’t come across this module, it allows the execution of Lua from within your Nginx server enabling the writing of scripts that can handle more complex situations. If this sounds interesting I would point you at a great free Nginx bundle that includes this module and many other extensions named OpenResty.

Now with great code comes great responsibility and it's the responsibility of an engineer to maintain code quality. A great way to do this is to write self-testing units of execution. Unit testing is a much needed tool in any developer's armoury, but applying the rules of unit testing, such as isolation, to scripts that spend most of their time communicating to the underlying web server is sometimes not a trivial task.

What follows is a small set of guidelines to aid in writing a suite of unit tests against your Nginx Lua scripts.

Use a testing framework

Every language has frameworks for testing and anything that allows you to write less code is a blessing. Lua unit testing is not an exception to this rule and the two main frameworks that seem to fill that space are Busted and Lunit. We chose Busted to help implement our tests as we preferred its API. If you are interested in Busted’s testy goodness check out its website.

Keep your Lua scripts small

Any good guide on unit testing will stress the need for small units of application code so that they can be tested more easily. I found this to be more difficult to achieve with scripting languages such as Lua due to their instantly executing script-like nature. This kind of lifecycle may make you think one big script is the simplest option but this will result in very large unmanageable tests. We found that the most easily tested code were the small cohesive methods in the utility classes we created. These had simple inputs and outputs and were shared by many scripts, so coverage in those areas brought a higher level of confidence. These utility classes also made mocking a lot easier.

Keep as little Lua code in your Nginx files as possible

You can embed your Lua code straight into the nginx files (such as nginx.conf) but we found that made it more difficult to test. You cannot execute non Lua scripts as tests and you cannot gain access to code in a private directives in the middle of your conf file. In order to get around this we tried to use the directives

{

content_by_lua_file lua_script.lua

access_by_lua_file lua_script.lua

}

to execute external Lua scripts as much as possible. You will probably struggle to remove all your Lua logic from the Nginx files but keeping the bulk of the code in separate scripts helps.

Manage dependencies

If you took my earlier advice you will find you now have a few dependant Lua files that you need to ‘require’ in each of your scripts. These along with the main Nginx API object ‘ngx’, will give you a handful of dependencies that you will need to manage in your tests. You will want to make a decision to either use the real scripts or use a technique such as mocking to swap them out for test doubles. In our case, due to more complex file structure on the server, all of our dependent scripts had to be mocked as they could not be loaded at test time. We invented a way of encapsulating all our dependencies during tests so we could swap them out to enforce test isolation.

Use the global namespace (_G)

Some of the objects available at runtime by Lua are objects in the global namespace. An example of one such item is the ‘ngx’ object that holds all the API functionality. This object is really _G.ngx and can be mocked out during tests. Using the mocking techniques in Busted you can return known results from functions and verify that certain methods were called with expected inputs.

Encapsulate your template variables in Lua

If you have a deployment pipeline, you may have a number of Lua files templated so that environment property values can be added. In order to allow us to tests these files, we had to remove the templating from many of the scripts and add a ‘property holder’ Lua object. This then became the only object that requiring templating and we could manage it as a dependency controlling properties during test execution.

Force the keyword ‘require’ to load your scripts multiple times

You will generally need to write more than one test against your scripts but Lua uses caching to optimize the loading of scripts when using the keyword ‘require’. Use the code snippet below to force your script out of the cache after each test so it will load and execute again.

{

  after_each(function()

     package.loaded['your qualified script name here '] = nil

   end)

}

What have we gained with our Lua test suite?

These simple steps enabled us to add our Nginx application to our deployment pipeline and bring our test coverage up to over 80% of the Lua code within it. Confidence is now much higher in the Lua application and we have assured code quality by protecting against software regression.

Test example

The following code snippet is a simple example of one of our Busted Lua specification tests that displays most of the characteristics suggested by this guide. Happy testing!

require 'busted.runner'()

describe("Nginx lua script tests", function()

 

   it("should cache and return key with valid message", function()

    --Given

    local message = {get_id = function() return 1 end}

    local returnedCacheKey = 'test'

    local helperUtils =  mock({cache = function() return returnedCacheKey end})

    local propertiesHolder = mock({get_enviroment = function() return 'DEV' end})

    _G.dependencyManager = {   --This is our dep manager implementation

       get_helper_utils = function() return helperUtils end ,

       get_properties = function() return propertiesHolder end      

     }

     _G.ngx = mock({utctime = function() return '11/13/15' end, log = function()end})

     

     --When

     local cacheKey = cacheMessage(message)

 

     --Then

     assert.are.equal(returnedCacheKey, cacheKey) --Buster framework asserts for result validation

     assert.spy(_G.dependencyManager.get_helper_utils().cache).was_called_with(message) -- Buster asserts for mock input validation

     assert.spy(_G.ngx.log).was_called()

   end)

end)

 

function cacheMessage(message)

 local cacheKey = dependencyManager.get_helper_utils().cache(message)

 if dependencyManager.get_properties().get_enviroment() == 'DEV' then

   ngx.log(1,'Message with id ' .. message.get_id() .. " was cached at " .. ngx.utctime())

 end

 return cacheKey

end

Subscribe to this blog

Use of this website and/or subscribing to this blog constitutes acceptance of the travel.cloud Privacy Policy.

Comments