What’s up today?

Today as a special Tuesday surprise, Tozier will be joining me, and we’ll work on tests. My guess is that we’ll shave the testing framework yak a bit, having produced a number of tests that presently run in Main. There are a number of ways we can go, and I think we’ll choose the path of evolving a test framework as we go. That will be more fun, for folks like us, and we’ll learn a bit more about the intricacies of Lua.1

Let’s review the testing code as it stands today:

-- S3 Spacewar

function setup()
    local offset = 200
    ship1 = Ship(vec2(WIDTH/2 + offset, HEIGHT/2), 0)
    ship2 = Ship(vec2(WIDTH/2 - offset, HEIGHT/2), 180)
    
    ship1.turn = 0.5
    ship2.turn = -0.4
    button1accel = Button(ship1, WIDTH-100, HEIGHT/5, "accelerate")
    parameter.action("Test Button", testButton)
--    testButton()
end

local testResult = ""
function testButton()
    local button = Button(nil, 100, 100, "nothing")
    local insideEndTouch    = { id=1, state=ENDED, x=100, y=100 }
    local outsideEndTouch   = { id=2, state=ENDED, x=151, y=100 }
    
    assertTrue(button:insideCircle(insideEndTouch), "should be inside")
    
    assertFalse(button:insideCircle(outsideEndTouch), "should be outside")
    
    assertEqual(button.capturedID, nil, "initial captured id should be nil")
    assertFalse(button.pressed, "button better not be pressed")
    
    testTouch(button, insideEndTouch,
        {function() assertEqual(button.capturedID, nil, "captured id should still be nil") end,
         function() assertFalse(button.pressed, "button still not pressed") end } )
    
    -- test that we do not lose a capture without an ENDED
    local insideBeganTouch  = { id=3, state=BEGAN, x=100, y=100 }
    local outsideBeganTouch = { id=4, state=BEGAN, x=151, y=100 }
    testTouch(button, insideBeganTouch,
        {function() assertEqual(button.capturedID, 3, "should capture the id") end } )
    testTouch(button, outsideBeganTouch,
        {function() assertEqual(button.capturedID, 3, "id should be 3") end ,
         function() assertFalse(button.pressed, "outside touch erroneously captured a press") end } )
    
    -- sequence test
    local closeThree = { id=3, state=ENDED, x=200, y=200 }
    local movingFinger = { id=5, state=BEGAN, x = 100, y=100}
    testTouch(button, closeThree,
        {function() assertEqual(button.capturedID, nil, "should have lost captured value") end} )
    testTouch(button, movingFinger,
        {function() assertEqual(button.capturedID, 5, "captured id should be 5") end,
         function() assertTrue(button.pressed, "now it is pressed") end } )
    movingFinger.state = MOVING
    testTouch(button, movingFinger, 
        { function() assertTrue(button.pressed, "moving inside should remain pressed") end } )
    movingFinger.x = 200
    testTouch(button, movingFinger,
        {function() assertEqual(button.capturedID, 5, "moving outside does not lose capture") end,
         function() assertFalse(button.pressed, "moving outside loses press") end
        }
    )
    
    print(testResult)
end

function testTouch(b, touch, tests)
    b:touched(touch)
    for _,test in ipairs(tests) do
        test()
    end
end

function touched(touchid)
    button1accel:touched(touchid)
end

function assertTrue( boolean, message )
    if not boolean then 
        testResult = testResult .. "\n" .. message .. "\n"
    else 
        testResult = testResult .. "."
    end
end

function assertFalse(boolean, message)
    return assertTrue( not boolean, message)
end

function assertEqual( actual, expected, message )
    return assertTrue(actual == expected, message)
end

This is basically everything from Main except the draw function. It’s not terribly badly factored, but of course none of this actually belongs in Main. It “should” be in a testing class of some kind. Doubtless the yak that really needs shaving is the one where this test class is a subclass of some basic testing framework class, with the asserts and such up there, yadda yadda. We’ll not get there today, unless we’re very lucky. But we’ll get to some better place.

Note, by the way, that little change at the top:

    parameter.action("Test Button", testButton)
--    testButton()

That’s an idea I stole yesterday from somewhere. It creates a button in the parameter space of the screen, and if you press the button, it calls the function testButton. The comment below it is the code that used to be there. Are you wondering why there are no parens after testButton in the parameter statement? That’s because testButton is the name of the function to be called, while testButton() would be an actual call. We want only to call when the button is pressed, and that’s how you do that. The screen now looks like this when we run:

IMAGE HERE

See the little line of dots in the output section? I pressed the button before I took the picture, and that’s the output of the tests. Note that we save the results, append a dot for each running test, and put in a diagnostic for a test that doesn’t work. I’ll add a failing test so you can see what happens:

    assertEqual(3, 2+2, "two plus two should be four")

IMAGE HERE

So that’s what happens: the output is a bit more compact. Less verbose. Doesn’t include so much information. Focuses on just the important things, the tests that are failing. Keeps you on your toes without droning on and on. You get the idea …

Let’s get to work

Our first move will be to blindly move all the testing-related code to a new class:

TestButton = class()

function TestButton:init(x)
end


local testResult = ""
function testButton()
    local button = Button(nil, 100, 100, "nothing")
    local insideEndTouch    = { id=1, state=ENDED, x=100, y=100 }
    local outsideEndTouch   = { id=2, state=ENDED, x=151, y=100 }
    
    assertTrue(button:insideCircle(insideEndTouch), "should be inside")
    
    assertFalse(button:insideCircle(outsideEndTouch), "should be outside")
    
    assertEqual(button.capturedID, nil, "initial captured id should be nil")
    assertFalse(button.pressed, "button better not be pressed")
    
    testTouch(button, insideEndTouch,
        {function() assertEqual(button.capturedID, nil, "captured id should still be nil") end,
         function() assertFalse(button.pressed, "button still not pressed") end } )
    
    -- test that we do not lose a capture without an ENDED
    local insideBeganTouch  = { id=3, state=BEGAN, x=100, y=100 }
    local outsideBeganTouch = { id=4, state=BEGAN, x=151, y=100 }
    testTouch(button, insideBeganTouch,
        {function() assertEqual(button.capturedID, 3, "should capture the id") end } )
    testTouch(button, outsideBeganTouch,
        {function() assertEqual(button.capturedID, 3, "id should be 3") end ,
         function() assertFalse(button.pressed, "outside touch erroneously captured a press") end } )
    
    -- sequence test
    local closeThree = { id=3, state=ENDED, x=200, y=200 }
    local movingFinger = { id=5, state=BEGAN, x = 100, y=100}
    testTouch(button, closeThree,
        {function() assertEqual(button.capturedID, nil, "should have lost captured value") end} )
    testTouch(button, movingFinger,
        {function() assertEqual(button.capturedID, 5, "captured id should be 5") end,
         function() assertTrue(button.pressed, "now it is pressed") end } )
    movingFinger.state = MOVING
    testTouch(button, movingFinger, 
        { function() assertTrue(button.pressed, "moving inside should remain pressed") end } )
    movingFinger.x = 200
    testTouch(button, movingFinger,
        {function() assertEqual(button.capturedID, 5, "moving outside does not lose capture") end,
         function() assertFalse(button.pressed, "moving outside loses press") end
        }
    )
    
    print(testResult)
end

function testTouch(b, touch, assertions)
    b:touched(touch)
    for _,assertion in ipairs(assertions) do
        assertion()
    end
end

function assertTrue( boolean, message )
    if not boolean then 
        testResult = testResult .. "\n" .. message .. "\n"
    else 
        testResult = testResult .. "."
    end
end

function assertFalse(boolean, message)
    return assertTrue( not boolean, message)
end

function assertEqual( actual, expected, message )
    return assertTrue(actual == expected, message)
end

Pressing the test button runs the tests. This surprised us for a moment until we realized that while we have all that code “in” a class, all the functions are still global. So the button found them. We need to make the tests all be methods of the class. As soon as we do this:

function TestButton:testButton()
...
end

Our parameter button does nothing. (Interestingly, it does not fail with a message, as an explicit call would. It just does nothing. We change it thus, still in main, and the tests run:

    parameter.action("Test Button", TestButton().testButton)

We need to do a few things, including:

  • Move the button into the test class;
  • Pull out some of the inner tests into separate tests;
  • Change more of the internal functions from global to be in the TestButton class;
  • Move the local result inside somehow;
  • Initialize it on the button press, because now if we press it multiple times we get a longer and longer string of dots;
  • Make the TestButton class find all those inner tests and run them.

The actual work will be to write a separate intentionally-breaking test, outside our main testButton function, and observe that it is not called. Then we’ll make the TestButton class smart enough to find all the tests inside itself. Then we can begin to pull out all the real tests one at a time. We add this would-be failing tests and it is not called. (We don’t really know that. We know it didn’t fail, and we think we know why. Are we correct? The answer may surprise you. At least if you’ve been paying attention.

function TestButton:testShouldFail()
    assertEqual(3, 2+2, "Two plus two is supposed to be four!")
end

Our button runs the tests, but we do not get the failing test. Our plan now is to point the button to a function in the TestButton class which collects up all the methods whose names begin with “test”, and call them. First, we change the button:

    parameter.action("Test Button", TestButton().runAllTests)

Now the button does nothing, because there is no runAllTests and parameter buttons appear not to care. We write runAllTests, make all the global functions in the class into methods, and use self: all over the place:

TestButton = class()

function TestButton:init()
end

function TestButton:runAllTests()
    self.result = ""
    for functionName, func in pairs(TestButton) do
        if string.find(functionName,"test") == 1 then
            func(self) 
        end
    end
    print(self.result)
end

function TestButton:testShouldFail()
    self:assertEqual(3, 2+2, "Two plus two is supposed to be four!")
end

function TestButton:testButton()
    local button = Button(nil, 100, 100, "nothing")
    local insideEndTouch    = { id=1, state=ENDED, x=100, y=100 }
    local outsideEndTouch   = { id=2, state=ENDED, x=151, y=100 }
    
    self:assertTrue(button:insideCircle(insideEndTouch), "should be inside")
    
    self:assertFalse(button:insideCircle(outsideEndTouch), "should be outside")
    
    self:assertEqual(button.capturedID, nil, "initial captured id should be nil")
    self:assertFalse(button.pressed, "button better not be pressed")
    
    testTouch(button, insideEndTouch,
        {function() self:assertEqual(button.capturedID, nil, "captured id should still be nil") end,
         function() self:assertFalse(button.pressed, "button still not pressed") end } )
    
    -- test that we do not lose a capture without an ENDED
    local insideBeganTouch  = { id=3, state=BEGAN, x=100, y=100 }
    local outsideBeganTouch = { id=4, state=BEGAN, x=151, y=100 }
    testTouch(button, insideBeganTouch,
        {function() self:assertEqual(button.capturedID, 3, "should capture the id") end } )
    testTouch(button, outsideBeganTouch,
        {function() self:assertEqual(button.capturedID, 3, "id should be 3") end ,
         function() self:assertFalse(button.pressed, "outside touch erroneously captured a press") end } )
    
    -- sequence test
    local closeThree = { id=3, state=ENDED, x=200, y=200 }
    local movingFinger = { id=5, state=BEGAN, x = 100, y=100}
    testTouch(button, closeThree,
        {function() self:assertEqual(button.capturedID, nil, "should have lost captured value") end} )
    testTouch(button, movingFinger,
        {function() self:assertEqual(button.capturedID, 5, "captured id should be 5") end,
         function() self:assertTrue(button.pressed, "now it is pressed") end } )
    movingFinger.state = MOVING
    testTouch(button, movingFinger, 
        { function() self:assertTrue(button.pressed, "moving inside should remain pressed") end } )
    movingFinger.x = 200
    testTouch(button, movingFinger,
        {function() self:assertEqual(button.capturedID, 5, "moving outside does not lose capture") end,
         function() self:assertFalse(button.pressed, "moving outside loses press") end
        }
    )
end

function testTouch(b, touch, assertions)
    b:touched(touch)
    for _,assertion in ipairs(assertions) do
        assertion()
    end
end

function TestButton:assertTrue( boolean, message )
    if not boolean then 
        self.result = self.result .. "\n" .. message .. "\n"
    else 
        self.result = self.result .. "."
    end
end

function TestButton:assertFalse(boolean, message)
    return self:assertTrue( not boolean, message)
end

function TestButton:assertEqual( actual, expected, message )
    return self:assertTrue(actual == expected, message)
end

We also had to move the result up into a member variable, you’ll notice. Now our should-fail test fails and the others all show the dots.

Frankly this was more editing than we were comfortable with, but the refactoring called for it, and fortunately we had tests that would have failed had we not done it right. By some miracle, we did it right. Now we have our tests moved to a testing class (named, by convention, Test[whatever]) and the class is finding everything that starts with “test” and running it. One concern would be that if we had a member variable called, oh, testCount or testResults, it would try to call it as if it were a test method. We will fix that when it happens.

So. Good for today: our tests are moved into a single test case. We can now pull out small tests from that one big one, one at a time. We’ll do that next time. See you in the sequel.

IMAGE HERE

  1. I am reminded that in some Heinlein novel, Podkayne of Mars if I’m not mistaken, the heroine, a young girl, having only read the word “intricacies”, thought it was pronounced “intrissiseas”. Which it isn’t, as we all know. But I digress.