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