Thursday, 9 December 2010

Testing Jump

One of the basic rules of TDD is to keep individual tests as simple as possible. Actually, I'm quite bad at this, and my tests tend to take on an epic character after a while. Still, if my test for conditional options depends on the jump tag working correctly, it makes sense to write a test for jump before I go any further

So:

--
-- test jump tag
--
function test_jump()
        local rc
        local scene = [[
        <scene Name="frogger">
         <narrative Name="lane_one">
          <snippet>truck approaching</snippet>
          <jump name="lane_two" />
          <snippet>***splat***</snippet>
         </narrative>
         <narrative Name="lane_two">
          <snippet> phew </snippet>
         </narrative>
        </scene>
        ]]

        local narrator = Narrator:new()
        local stage = TestStage:new()
        narrator:load(scene)
end


So, if the jump works, Frogger winds up in lane two and dodges the truck. If not ... well I expect he gets used to it after a while. Anyway, this test fails with the same error as the last one. So now we need to fix that error. That means telling Narrative about jump tags


function Narrative:do_item(raw_item)
        local item = nil
        local auto_cut = false

        assert(raw_item.label ~= nil)
        local label = raw_item.label:lower()

        if label == "snippet" then
                item = Snippet:new(raw_item)
                if item.image ~= nil then
                        auto_cut = true
                end

        elseif label == "jump" then
                -- jump code to go here



Another TDD convention is to make the least change to fix the error. So that won't make the jump work, but it will get us past the current error. And in fact the test now passes. Mainly because we haven't checked to see if the jump works.

So let's add a play() call to the test.

        --
        -- play the scene. I expect an auto-cut after the jump
        -- which means play should return true
        --
        rc = narrator:play(stage)
        assert_true(rc)


And that fails again. play() consumes the entire narrative and drops off the end thinking "my work here is done". So I need an item in the input queue with a cut=true element. (I've only documented cut for snippets, and they remain the only element where the XML parser accepts cut as an attribute - another reason for making Cut an element in in its own right).

The Jump class is about as simple as it gets.  At least if you ignore the XML parsing code which I don't want to get into right now. It's based on the 5.1 example on the Lua wiki if anyone's interested). Anyway: Jump class:

Jump = Jump or {}
Jump.__index = Jump

function Jump:new(xml)
        local self = { }
        setmetatable(self, Option)

        self.target = xml.xarg.target
        self.cut = true

        return self
end

return Jump


Following on, we just need to create an instance in the Narrative loop

        elseif label == "jump" then
                item = Jump:new(raw_item)


A bit of fiddling and ...the test fails! I've told Narrative where to put the Jump  object, but not told Narrator what to do with it.

I can get that error to go away (and the test to pass) by adding a couple of lines to Narrator

       while true do
                self.index = self.index + 1

                item = self.narrative.items[self.index]
                if item == nil then
                        return false
                end
--
--              anything can set an image, it seems
--              (argument for making "image" a narrative element?
--              would solve the cut problem, certainly)
--
                print("item.type = " .. item.type)
                if item.type == "snippet" then
                        self:do_snippet(interface, item)

                elseif item.type == "image" then
                        interface:set_background(item:filename())

                elseif item.type == "jump" then
                        -- code to go here


And that passes the test. On the other hand, we aren't testing yet to see if the jump is executed. We need to add to the test case for that...

        --
        -- OK: we just did the jump. Next play should run the new narrative
        --
        rc = narrator:play(stage)
        assert_false(rc)                        -- this should be the end

        assert_equal(1, stage:num_oq())         -- expect one item in output Q
        assert_equal("text", stage:oq(1).type)
        assert_not_equal(                       -- A tragedy averted?
                "*** Splat ***",
                stage:oq(1).value
        )

        assert_equal(                          -- just to make sure
                "phew",
                stage:oq(1).value
        )


Alas, poor Frogger...

  1) Failure (narrator_tests.test_jump):
test_narrator.lua:637: '*** Splat ***' not expected but was one


So, finally, I get to the business of making the actual jump work. We already change narratives in the option code. Rather than copy and paste that, let's put it in its own function

function Narrator:change_narrative(name)
        self.narrative = self.scene.narratives[ name ]
        self.index = 0
end


I'm tempted to add some error checking to that, but it'll do for now.

                elseif item.type == "jump" then
                        self:change_narrative(item.target)


And that doesn't work. It's not jumping and it's not returning false. Putting some checks into the function suddenly looks like a good idea

function Narrator:change_narrative(target)
        if target == nil then
                error("Narrator:change_narrative: target is nil!")
        end

        local temp = self.scene.narratives[ target ]
        if temp == nil then
                error("Narrator:change_narrative: narrative '" .. target .. "' i
s nil")
        end

        self.narrative = temp
        self.index = 0
end


Re-running the tests, I get

117 Assertions checked.

  1) Error! (narrator_tests.test_jump):
../Scripts/Narrator.lua:38: Narrator:change_narrative: target is nil!

So the target attribute isn't being set - which turns out to be because the XML in the test is wrong. I changed my mind about the attribute that specifies the destination narrative, changing the attribute name from "name" to "target". I didn't update the test XML however.

Once that's fixed, all is well.

I'm tempted to add some validation code the the Jump constructor to catch the case if it happens again, maybe write a few test cases to make sure it handles the error cases correctly... but I'll leave that until such time as XML errors become a problem. I still have Conditional options to get working!

No comments: