Unit testing in the Translator

If you’re like me, you love unit testing. If you don’t love it, hopefully you at least recognize its usefulness. I have created a module for the Translator, unittest, which lets me test new features we add to the API.  It can also help you unit test your own code.  Here is the code for the module:

unittest = {}

local Output = ''

-- Local function declarations
local RunFunctionsInTable
local Out
local RunFunction
local TableSize

----------------------
-- Begin Public API --
----------------------

-- Add functions (or other tables containing functions) to this table
-- To make the tests run in RunAll().
unittest.case = {}

-- Run all test cases.  It would be easy enough to implement a RunSome()
-- function which would accept some sort of string filter.
-- Returns a pass/fail report, as a string.
function unittest.RunAll()
   Output = ''
   RunFunctionsInTable(unittest.case, 'unittest.case')
   return Output
end

function unittest.AssertTrue(Value)
   if not Value then
      error('Expression was not true.', 2)
   end
end

function unittest.AssertEquals(Expected, Actual, LevelInc)
   if not LevelInc then LevelInc = 0 end
   if Expected ~= Actual then
      error('Expected did not match Actual.n'..
               '      Expected: ['..tostring(Expected)..']n'..
               '      Actual:   ['..tostring(Actual)..']',
               2 + LevelInc)
   end
end

-- Note that #T only works well for tables
-- with integer indices (ie, arrays).
-- The following two functions will work for
-- tables with any keys.
function unittest.AssertTableSize(ExpectedSize, T)
   unittest.AssertEquals(ExpectedSize, TableSize(T), 1)
end

function unittest.AssertTableSizes(T1, T2)
   unittest.AssertEquals(TableSize(T1), TableSize(T2), 1)
end

-- Supports calling functions with 0 or 1 parameters.
-- ExpectedErr is optional.
function unittest.AssertError(F, Arg, ExpectedErr)
   local Status, ActualErr = pcall(F, Arg)
   if Status then
      error('Function did not throw an error.', 2)
   else
      if ExpectedErr ~= nil then
         if ExpectedErr ~= ActualErr then
            print(ExpectedErr, ActualErr)
            error('Function did not throw expected error.n'..
                     '      Expected: ['..tostring(ExpectedErr)..']n'..
                     '      Actual:   ['..tostring(ActualErr)..']', 2)
         end
      end
      -- else, the fact than an error was thrown is good enough
   end
end

--------------------
-- End Public API --
--------------------

function RunFunctionsInTable(T, NamePrefix)
   for K, V in pairs(T) do
      if type(K) == 'string' then
         if type(V) == 'function' then
            RunFunction(K, V, NamePrefix)
         elseif type(V) == 'table' then
            RunFunctionsInTable(V, NamePrefix..'.'..K)
         end
      end
   end
end

function Out(Text)
   Output = Output..Text
end

function RunFunction(N, F, NamePrefix)
   local Status, Err = pcall(F)
   if Status then
      Out('[PASS: '..NamePrefix..'.'..N..']n')
   else
      Out('[FAIL: '..NamePrefix..'.'..N..']:n   '..Err..'n')
   end
end

function TableSize(T)
   local Size = 0
   for K, V in pairs(T) do
      Size = Size + 1
   end
   return Size
end

You don’t have to understand the inner workings of the code to understand how to use it. Here is a very basic demonstration of how to use the module:

require('unittest')

-- The main function is the first function called from Iguana.
function main()
   unittest.RunAll()
end

function unittest.case.testBooleans()
   unittest.AssertTrue(true)
   unittest.AssertTrue(not false)
end

function unittest.case.testNumbers()
   unittest.AssertEquals(4, 2+2)
   unittest.AssertEquals(5, 2+2)
end

function unittest.case.testStrings()
   local Foo = 'foo'
   unittest.AssertEquals('oo', Foo:sub(2))
   unittest.AssertEquals('foobar', Foo..'bar')
end

local function MyFunction(A)
   if type(A) ~= 'number' then
      error('MyFunction requires a number argument')
   end
end

function unittest.case.testErrors()
   MyFunction(2)
   unittest.AssertError(MyFunction, 'a string')
   unittest.AssertError(MyFunction, 'a string', 'MyFunction requires a number argument')
end

function unittest.case.unexpectedError()
   MyFunction('a string')
end

function unittest.case.testTables()
   local Foo = {}
   unittest.AssertTableSize(0, Foo)
   Foo[1] = 'A'
   unittest.AssertTableSize(1, Foo)
   local Bar = {'B'}
   unittest.AssertTableSizes(Bar, Foo)
end

With the above code in the Translator, you can click on the result from unittest.RunAll() (in main), and you will get the following nicely formatted output:

[FAIL: unittest.case.unexpectedError]:
   MyFunction requires a number argument
[PASS: unittest.case.testStrings]
[FAIL: unittest.case.testNumbers]:
   [string "main"]:15: Expected did not match Actual.
      Expected: [5]
      Actual:   [4]
[PASS: unittest.case.testTables]
[PASS: unittest.case.testErrors]
[PASS: unittest.case.testBooleans]

The easiest way to set up your unit tests is as follows:

  1. Create a brand new channel, dedicated to the unit tests.  I find it’s easiest to create a “From Translator” > “To Channel” channel.
  2. Edit the script for the “From Translator” component.
  3. Add the unittest module to the project.
  4. Add require(‘unittest’) to the top of the main file.
  5. Add unittest.RunAll() to the main() function.
  6. Add functions beginning with ‘unittest.case.’ (see the examples above) to make the functions executed as a unit test.

Note: The function order in the results does not match the order in which the functions are defined, this is something to be aware of.

– Kevin Senn, Chief Design Officer iNTERFACEWARE

Leave A Comment?