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:
- Create a brand new channel, dedicated to the unit tests. I find it’s easiest to create a “From Translator” > “To Channel” channel.
- Edit the script for the “From Translator” component.
- Add the unittest module to the project.
- Add require(‘unittest’) to the top of the main file.
- Add unittest.RunAll() to the main() function.
- 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