Alternate CodeaUnit

A modified version of @jakesankey’s extremely helpful CodeaUnit.

Somebody might find this useful, as it’s been tweaked to do a few convenient things:

  • The formatting of the results is modified to make it easier to scroll through when there are lots of tests happening at the same time.
  • The pass/fail status of any given assertion has been given greater emphasis to make it easier to spot failed tests while scrolling.
  • Multiple assertions inside a single test can each have their own labels which will be included in the printout.
  • Multiple assertions inside a single test are automatically numbered in the style “1a, 1b, 1c, etc” to make it easier to identify which test they belong to.

It’s been working for me, but I haven’t fully tested it (ironically), so there might still be a glitch here or there. If you find one please let me know.


--from an original by jakesankey

CodeaUnit = class()
CodeaUnit.isRunning = false
CodeaUnit.doBeforeAndAfter = true

function CodeaUnit:describe(feature, allTests)
    print(string.format("\\t****\
\\t%s\
\\t****", feature))
    if self.skip == true then
        print("\\t * Tests Skipped")
    else
        self.tests = 0
        self.subtests = 0
        self.totalTests = 0
        self.ignored = 0
        self.failures = 0
        self.message = "message not set"
        self.debugReporting = false
        self._before = function()
        end
        self._after = function()
        end
        
        allTests()
        
        local passed = self.totalTests - self.failures
        local summary = string.format("\\t\\t\\t----------\
\\t\\t\\tPass: %d\
\\t\\t\\tIgnore: %d\
\\t\\t\\tFail: %d", passed, self.ignored, self.failures)
        
        print(summary)
    end
end

function CodeaUnit:before(setup)
    self._before = setup
end

function CodeaUnit:after(teardown)
    self._after = teardown
end

function CodeaUnit:ignore(description, scenario)
    self.description = tostring(description or "")
    self.tests = self.tests + 1
    self.ignored = self.ignored + 1
    if CodeaUnit.detailed then
        print(string.format("%d: %s -- Ignored", self.tests, self.description))
    end
end

function CodeaUnit:test(description, scenario)
    
    self.tests = self.tests + 1
    self.totalTests = self.totalTests + 1
    self._before()
    
    local beforeString = "__________________\
*** First. CodeaUnit:test(...) description before assignment: "..tostring(self.description)
    self.description = tostring(description or "")
    local afterString = "*** CodeaUnit:test(...) description after assignment: "..tostring(self.description)
    if self.debugReporting then
        print(string.format("%s\
%s", beforeString, afterString))
    end    
    
    local status, err = pcall(scenario)
    if err then
        self.failures = self.failures + 1
        print(string.format("%d: %s -- %s", self.tests, self.description, err))
    end
    self._after()
    if self.subtests ~= 0 then
        self.totalTests = self.totalTests + self.subtests - 1
    end
    self.subtests = 0
    self.description = nil
    self.message = nil
end

--function CodeaUnit:expect(conditional)
--takes one or two arguments
--can take just the expected value, or a name for this individual 'expect' call plus the expected value
--this allows multiple 'expect' calls in a single test to all show different titles
function CodeaUnit:expect(...)
    
    local encoding = "abcdefghijklmnopqrstuvwxyz"
    local function letterFromNum(i)
        return encoding:sub(i,i)
    end

    --detecting #args will mess up if expected value has returned nil, because nil isn't counted as a value
    local args = {...}
    if #args == 2 then multiTest = true end 
    local unpackedArgs = tostring(table.unpack(args))
    if self.debugReporting then
        local descriptionString = "*** Second. CodeaUnit:expect(...) self.description: "..tostring(self.description)
        local argsExplained = "*** CodeaUnit:expect(...) args: "..#args..", unpacked: "..unpackedArgs
        print(string.format("%s\
%s", descriptionString, argsExplained))
    end
    self.message = string.format("%d. %s:", (self.tests or 1), self.description)
    if not multiTest then
        conditional = args[1]
    elseif #args == 2 then
        local premessage = ""
        self.subtests = self.subtests + 1
        if self.subtests == 1 then
            premessage = string.format("%s\
\
", self.message)
        end
        conditional = args[2]
        self.message = string.format("%s %d.%s. %s", premessage, (self.tests or 1), letterFromNum(self.subtests), args[1])
    end
    
    local passed = function()
        if CodeaUnit.detailed then
            print(string.format("%s\
  Expected: %s\
  -- OK", self.message, self.expected))
        end
    end
    
    local failed = function()
        self.failures = self.failures + 1
        local actual = tostring(conditional)
        local expected = tostring(self.expected)
        print(string.format("%s\
  Expected: %s\
  -- FAIL: found %s", self.message, expected, actual))
    end
    
    local notify = function(result)
        if self.debugReporting then
            print("notify() message: "..tostring(self.message)..", self.expected: "..tostring(self.expected))
        end
        if result then
            passed()
        else
            failed()
        end
    end
    
    local is = function(expected)
        self.expected = expected 
        notify(conditional == expected)
    end
    
    local isnt = function(expected)
        self.expected = expected
        notify(conditional ~= expected)
    end
    
    local has = function(expected)
        self.expected = expected
        local found = false
        for i,v in pairs(conditional) do
            if v == expected then
                found = true
            end
        end
        if not found then
            conditional = "no such value"
        end
        notify(found)
    end
    
    local throws = function(expected)
        self.expected = expected
        local status, error = pcall(conditional)
        if not error then
            conditional = "nothing thrown"
            notify(false)
        else
            notify(string.find(error, expected, 1, true))
        end
    end
    
    return {
        is = is,
        isnt = isnt,
        has = has,
        throws = throws
    }
end

CodeaUnit.execute = function()
    CodeaUnit.isRunning = true
    for i,v in pairs(listProjectTabs()) do
        local source = readProjectTab(v)
        for match in string.gmatch(source, "function%s-(test.-%(%))") do
            load(match)()
        end
    end
end

CodeaUnit.detailed = true



_ = CodeaUnit()

parameter.action("CodeaUnit Runner", function()
    CodeaUnit.execute()
end)


And here’s an example of using it:


function testEasyCraft()
    
    CodeaUnit.detailed = true
    CodeaUnit.skip = false
    
    _:describe("Testing EasyCraft", function()
        _:before(function()
        end)
        _:after(function()
        end)
        
        
        _:test("makeAThing() creates entity when called without parameters", function()
            local entity = EasyCraft.makeAThing()
            _:expect(tostring(entity)).is("entity")
        end)
        
        
        _:test("makeAThing() creates correct name, position, rotation, scale, and model", function()
            local name = "Joe Entity"
            local position = vec3(19, 22, 3.333)
            local rotation = vec3(-26.973995, -30.679983, -28.186922)
            local eScale = vec3(99, 5, 2)
            local modelPack = "Watercraft"
            local modelName = "watercraftPack_003_obj"
            local entity = EasyCraft.makeAThing(name, modelPack, modelName, position, rotation, eScale)
            local angleAdjuster = scene:entity()
            angleAdjuster.eulerAngles = vec3(-26.973995, -30.679983, -28.186922)
            local regurgutatedEulers = angleAdjuster.eulerAngles
            _:expect("name assignment", entity.name).is(name)
            _:expect("position assignment", tostring(entity.position)).is(tostring(position))
            _:expect("rotation assignment", tostring(entity.eulerAngles)).is(tostring(regurgutatedEulers))
            _:expect("scale assignment", tostring(entity.scale)).is(tostring(eScale))
            _:expect("modelName", entity.modelName).is(modelName)
        end)

        
        _:test("reading, loading, and calling a function saved to a tab works", function()
            local stringToReturn = "anotherMeaninglessString"
            saveProjectTab("meaninglessTab", [[
function meaninglessFunction()
    local anotherMeaninglessString = ']]..stringToReturn..[['
    return anotherMeaninglessString
end]])
        local tab = readProjectTab("meaninglessTab")
        load(tab)()
        local result = meaninglessFunction()
        saveProjectTab("meaninglessTab", nil) --deletes
        _:expect(stringToReturn).is(result)
    end)
 end

Interesting, I’ll check it out. I should probably publish the one I’m using.

One thing I did differently is that I put the description of the test as the second, optional argument to “expect”, which makes the code easier, as you can just default it rather than count the args etc.


function CodeaUnit:expect(conditional, msg)
    local message = string.format("%d: %s %s", (self.tests or 1), self.description, (msg or ""))
    
    local passed = function()
        if CodeaUnit_Detailed then
            print(string.format("%s -- OK", message))
        end
    end

@RonJeffries oooooo that looks sweet.

All the CodeaUnit code makes my head spin, honestly, and it was really hard for me to get my changes to work at all, so I’m a bit scared to mess with it, but this seems like a really good idea.

And it fits your general principle that if something is too complicated, you’re probably doing it the wrong way.