Asteroids 4: CodeaUnit
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:
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!