I needed to test some floats the other day. Let’s improve CodeaUnit to help with that.

CodeaUnit has only a very limited number of assertions: is, isnt, has, hasnt, and throws. The is and isnt assertions test equality. For floating point tests, we need a little leeway, since, well, floating point calculations aren’t often exact.

My cunning plan is to improve CodeaUnit by adding an optional parameter to is, to allow a small error amount when desired. Let’s look at CodeaUnit in its entirety:


CodeaUnit = class()

function CodeaUnit:describe(feature, allTests)
    self.tests = 0
    self.ignored = 0
    self.failures = 0
    self._before = function()
    end
    self._after = function()
    end

    print(string.format("Feature: %s", feature))

    allTests()

    local passed = self.tests - self.failures - self.ignored
    local summary = string.format("%d Passed, %d Ignored, %d Failed", passed, self.ignored, self.failures)

    print(summary)
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.description = tostring(description or "")
    self.tests = self.tests + 1
    self._before()
    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()
end

function CodeaUnit:expect(conditional)
    local message = string.format("%d: %s", (self.tests or 1), self.description)

    local passed = function()
        if CodeaUnit.detailed then
            print(string.format("%s -- OK", message))
        end
    end

    local failed = function()
        self.failures = self.failures + 1
        local actual = tostring(conditional)
        local expected = tostring(self.expected)
        print(string.format("%s -- Actual: %s, Expected: %s", message, actual, expected))
    end

    local notify = function(result)
        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
        notify(found)
    end

    local hasnt = function(expected)
        self.expected = expected
        local missing = true
        for i,v in pairs(conditional) do
            if v == expected then
                missing = false
            end
        end
        notify(missing)
    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,
        hasnt = hasnt,
        throws = throws
    }
end

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

CodeaUnit.detailed = true



_ = CodeaUnit()

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

Now this little baby is rather interesting. I’ll explain it briefly but it’s probably more than anyone wants to know.

It all starts when you press the “CodeaUnit Runner” button in the Codea parameter space:

button

As advertised, this runs the execute function, which loops over all the tabs in the project and finds functions whose names begin with “test”. It loads these functions as it finds them, and executes them. A typical function will look like the one we wrote in our Asteroids program:

function testAsteroids()
    CodeaUnit.detailed = true

    _:describe("Asteroids First Tests", function()

        _:before(function()
            -- Some setup
        end)

        _:after(function()
            -- Some teardown
        end)
        
        _:test("Hookup", function()
            _:expect( 2+1 ).is(3)
        end)
        
        _:test("Random", function()
            local min = 100
            local max = 0
            for i = 0,1000 do
                local rand = math.random()*2*math.pi
                if rand < min then min = rand end
                if rand > max then max = rand end
            end
            _:expect(min < 0.01).is(true)
            _:expect(max > 6.2).is(true)
        end)
        
        _:test("Rotated Length", function()
            for i = 0, 1000 do
                local rand = math.random()*2*math.pi
                local v = vec2(1.5,0):rotate(rand)
                local d = v:len()
                _:expect(d > 1.495).is(true)
                _:expect(d < 1.505).is(true)
            end
        end)
        
        _:test("Some rotates go down", function()
            local angle = math.rad(-45)
            local v = vec2(1,0):rotate(angle)
            local rvx = v.x*1000//1
            local rvy = v.y*1000//1
            _:expect(rvx).is(707)
            _:expect(rvy).is(-708)
        end)
        
        _:test("Bounds function", function()
            _:expect(putInBounds(100,1000)).is(100)
            _:expect(putInBounds(1000,1000)).is(0)
            _:expect(putInBounds(1001,1000)).is(1)
            _:expect(putInBounds(-1,1000)).is(999)
        end)

    end)
end

The test is expected to begin by calling _:describe, which is shorthand for CodeaUnit:describe, because CodeaUnit said _ = CodeaUnit. The underbar variable is commonly used for scratch values so this probably works. I suspect code that uses _ for scratch would have problems. But that’s not our concern just now.

Note that describe has a parameter allTests which it calls. And in the test, as shown above, the entire rest of the test suite is contained in that function allTests.

And everything in there, before, after, test is a method on CodeaUnit that has as its parameter, another function! Wow. There is some initial flow, where the before and after functions are tucked away in self._before and self._after, and then we hit the tests, which are processed by CodeaUnit:test:

function CodeaUnit:test(description, scenario)
    self.description = tostring(description or "")
    self.tests = self.tests + 1
    self._before()
    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()
end

This saves away the description as a string (I suppose in case it was a number or something), increments the test count, calls the saved before function, and runs the test function with pcall. This is a protected call that will return an error if the test fails or hurls.

The tests all look similar, and as far as I’ve figured out, this is the only option.

You do some setup, calculate whatever, then you can have one or more lines like:

_:expect(someExpression).is(expectedValue)

Or you can use the other checks, isnt, has, and so on. The has expects to be given a table, and looks in the table to see if the provided value appears. And so on.

So CodeaUnit quite intricate and quite elegant in its understanding of how Lua works. My hat’s off to its author. jakesankey.

Our mission is to extend is to accept an optional second parameter, epsilon, to require that the computed value needs to be within a difference of epsilon of the expected.

I’ve set up CodeaUnit as a repo in WorkingCopy, so that I can revert as needed, er I mean so that our excellent changes can be preserved. To do this change, I’ll include the CodeaUnit Example tab into the CodeaUnit project, extend that to test some floats, make it work, then commit the code and remove the tab. We don’t want CodeaUnit to execute its own tests every tine we use it. And I guess I’ll paste the extended test back into the Example project.

This is all a bit tricky because of how Codea deals with dependencies, at the project level not the tab level.

Here goes, hold my Diet Coke. Here’s the CodeaUnit example:


function testCodeaUnitFunctionality()
    CodeaUnit.detailed = true

    _:describe("CodeaUnit Test Suite", function()

        _:before(function()
            -- Some setup
        end)

        _:after(function()
            -- Some teardown
        end)

        _:test("Equality test", function()
            _:expect("Foo").is("Foo")
        end)

        _:test("Negation test", function()
            _:expect("Bar").isnt("Foo")
        end)

        _:test("Containment test", function()
            _:expect({"Foo", "Bar", "Baz"}).has("Foo")
        end)

        _:test("Thrown test", function()
            _:expect(function()
                error("Foo error")
            end).throws("Foo error")
        end)

        _:ignore("Ignored test", function()
            _:expect("Foo").is("Foo")
        end)

        _:test("Failing test", function()
            _:expect("Foo").is("Bar")
        end)

    end)
end

Here’s my test:

        _:test("Floating point epsilon", function()
            _:expect(1.45).is(1.5, 0.1)
        end)

I’ve added the second parameter, 0.1, signifying that I will accept any answer within 0.1 of the expected. This test will fail, I hope.

Sure enough, it says “Floating point epsilon, Expected 1.5, Actual 1.45”

Now to fix is, which looks like this:

    local is = function(expected)
        self.expected = expected
        notify(conditional == expected)
    end

Since is can test any types, we can’t do arithmetic unless our new parameter is provided. Let’s try this:

    local is = function(expected, epsilon)
        self.expected = expected
        if epsilon then
            notify(expected - epsilon <= conditional and conditional <= expected + epsilon)
        else
            notify(conditional == expected)
        end
    end

We just check to see if epsilon is there (nil is falsy) and if it is check for within range, otherwise check for equality. Nice, simple, and works. We could write more tests but I’m confident enough.

What about isnt? Well, expect(1.45).isnt(1.5) already fails. I don’t see a need for a test that means something like “be sure this isn’t even close”, so I think I’ll let this be. We could add a second illegal parameter to isnt but that seems really weird.

I’m calling this done, updating the example, removing the tab, and committing.

See you next time!