TDD is too big a lump to be a good strawberry. Today I’m playing with TDD to try to find a bite-sized goodie. I expect not to accomplish that.

This morning, as I resisted getting up, I was thinking about an odd property that TDD has. We are usually trying to write some general purpose thing, yet we test it at just a few points in its behavior. Yet, somehow, we expect to wind up with the general purpose result. As a rule, we usually do.

In the olden days of TDD, people would ask questions like “But you could just write a special case chunk of code that just checks for the inputs from the tests and returns the right answer. How can that possibly work?”

Depending on our personalities, we generally answered with variously polite responses that came down to something like “We don’t do that because we’re not quite dull enough to try to trick ourselves into doing something that stupid.”

So I was thinking about a curve of increasing program capability, with a growing number of test cases that pin down that curve at certain points, the point that we test. Something like this picture:

weird probes on a curve

Curves made me think of a possibly interesting little thing to develop with TDD, a polynomial evaluator. The story might be something like this:

Create an object, Poly, that can be given the coefficients of a single-variable polynomial in x, and which provides a single method to evaluate the polynomial at any given value of x. The object should work for any order of polynomial.

So this morning I’m going to TDD this, and I’m going to pretend not to be very bright about polynomials. This will not be difficult for me.

Begin with a Test

Since our topic is TDD, I’ll want to begin with a test. I create a standard CodeaUnit-based Codea project, Polynomials, and wire up the initial hookup test. It runs:

loading	testPolynomials()
Feature: Polynomials
1: HOOKUP testing fooness -- Actual: Foo, Expected: Bar
0 Passed, 0 Ignored, 1 Failed

Sweet. I’ll replace the hookup test with my first polynomial test. I plan to use the simplest possibly polynomial, the one that is a constant.

        _:test("Constant", function()
            local poly = Poly(5)
            _:expect(poly:at(1)).is(5)
        end)

Let me say right here that I have written more of a test than I need in order to get a failure. I could have just used the first line, which would drive out this message:

1: Constant -- Tests:15: attempt to call a nil value (global 'Poly')

Then I could have implemented a Poly class and passed the no-assert test. Then I could add the expect and have the at fail, then add at and have the 5 not come out, and so on.

I’m not here to promote the strict version of “never write a line of code without a running test, and never write more of a test than you need to get an error”. I think in chunks, and I write in chunks. We’re after different game here.

But I will go step by step in my implementation.

Poly = class()

This elicits:

1: Constant -- Tests:16: attempt to call a nil value (method 'at')

No real surprise. But I do often like to let the test, however large it is, unfold bit by bit. I enjoy the part where the computer tells me what to do.

Now I could implement the at function to return 5, or I could do the real thing. Let’s return 5, just to be obtuse.

function Poly:at(x)
    return 5
end

I reckon this will pass the test.

1: Constant  -- OK

Normally, I’d extend this test but let’s have another instead, just to keep them tiny.

        _:test("Constant 5", function()
            local poly = Poly(5)
            _:expect(poly:at(1)).is(5)
        end)
        
        _:test("Constant 7", function()
            local poly = Poly(7)
            _:expect(poly:at(3)).is(7)
        end)

This will fail expecting 7:

2: Constant 7  -- Actual: 5, Expected: 7

Let’s do something that will work for both …

Poly = class()

function Poly:init(a)
    self.a = a
end

function Poly:at(x)
    return self.a
end

Now both tests pass. So long as we pretend this is a hard problem that we don’t understand, or a series of stories, it almost makes sense. Of course, for a polynomial, we know this isn’t a very useful scheme. Let’s allow our tests to pull us forward into the future for a while.

Two Coefficients

So we figure we have this one-coefficient constant thing working, so we decide to try a really hard one: a polynomial of degree one rather than zero. We’ll try y = 2*x+5.

        _:test("2x+5", function()
            local poly = Poly(2,5)
            _:expect(poly:at(0)).is(5)
            _:expect(poly:at(1)).is(7)
        end)

This will fail, returning 2 for both, I figure.

3: 2x+5  -- Actual: 2, Expected: 5
3: 2x+5  -- Actual: 2, Expected: 7

Now, I’m still not clever enough to see the big picture here, but I can see that I need something like this:

function Poly:init(a,b)
    self.a = a
    self.b = b
end

function Poly:at(x)
    return 0 -- something about a and b and x
end

A bit of thinking tells me that I should deal with missing parameters by treating them as zero, but I don’t see how to deal with the situation unless I check the number of parameters that I have … or maybe I could require the user to provide his coefficients in reverse order.

Checking with my polynomial person, I am informed that, no, they want the parameters in what they call “natural order”. Bummer.

I have to redesign this thing to even make the 2x+5 test pass. But I can kludge it.

function Poly:at(x)
    if self.b then
        return self.a*x + self.b
    else
        return self.a
    end
end

This passes the test, by checking which of the two cases we have.

Now that my tests run, I can work toward a better design. But first we need to digress.

DIgression–or is it?

We actually encounter this kind of situation in real product development, quite frequently. We get a new requirement and our old code won’t handle it, but we bash on it a bit with a hammer until the new feature is in.

We could keep bashing with our hammer for a long time. There’s almost always a bigger hammer to be had. But the fact is, our current design isn’t very good. Oh, it could be made to work up to whatever number of parameters we wanted to deal with. But we really need to make it better.

As yet, we don’t see how to do that. Well, after some thinking, we go ahead with our next story.

Quadratic

        _:test("3x^2 +5x+7", function()
            local poly = Poly(3,5,7)
            _:expect(poly:at(0)).is(7)
            _:expect(poly:at(2)).is(3*2*2 + 5*2 + 7)
        end)

I was counting on my fingers to get that last result, but I decided to let the computer help me. I actually wrote 12+10+7, doing the bits in my head, but then I thought, no, why not show my work, and I wrote 3*4+ and thought, no, why not show all my work, and wrote the full expression out.

Test will fail of course.

4: 3x^2 +5x+7  -- Actual: 5, Expected: 7
4: 3x^2 +5x+7  -- Actual: 11, Expected: 29

Time to make the code work better. I decide to do this:

function Poly:init(a,b,c)
    self.a = a
    self.b = b
    self.c = c
end

function Poly:at(x)
    if self.c then
        return self.a*x*x + self.b*x + self.c
    elseif self.b then
        return self.a*x + self.b
    else
        return self.a
    end
end

The tests pass. I’m pretty proud of this and it’s really clear now that I can keep on doing this forever, for however many coefficients they want. I ask the polynomial person how many that is, planning my if statements, and they say “Lots, maybe over 100. It should work for whatever we provide.”

Bummer. I don’t really want to type in that many if statements. I briefly consider generating the code on the fly and compiling it, but that seems wrong somehow.

We need a better idea. But we must digress again.

Digression 2–or is it?

Here’s where we come to appreciate our tests. They’ve already been quite helpful. I didn’t mention this but when I did the quadratic one, I forgot to save the c parameter, and the tests failed until I did it. I also typed a b and c a few times, forgetting the self., so the tests have been very useful in finding silly errors.

But now … we have to change the whole approach, and we’re not even sure just what needs to be done.

We need a mathematical insight, a well-known one, but we’re using this example to stand in for the situation where we don’t know a magical answer, so we have to work to find out what to do.

Our tests are going to allow us to experiment until we find a better more general design, even if it takes many tries.

We’ll compress time here, because we have a simple program. This story, on a large product, would be the same even if stretched out over weeks or months of learning and improvement.

Design Improvement Over Time

One thing seems obvious … we’re going to have to treat our input parameters as an array, since we can’t write a thousand if statements. We could, but the testers would then test 1003 and break everything. So first we convert to an array just to see if it tells us anything. The way we do that in the constructor is this:

function Poly:init(...)
    self.args = {...}
end

The … represents all the parms we send in, unwrapped. We make an array of them and save it in args.

Now our code will fail badly, because our only method looks like this:

function Poly:at(x)
    if self.c then
        return self.a*x*x + self.b*x + self.c
    elseif self.b then
        return self.a*x + self.b
    else
        return self.a
    end
end

We don’t have a, b, c any more, but we can plug in the array values:

function Poly:at(x)
    if self.args[3] then
        return self.args[1]*x*x + self.args[2]*x + self.args[3]
    elseif self.args[2] then
        return self.args[1]*x + self.args[2]
    else
        return self.args[1]
    end
end

The tests confirm that this is correct. We begin to see a pattern, but it’s not entirely clear. Maybe I can unwind it somehow. Let’s see … is there some way we could reverse the order of our checking, go 1,2,3 instead of 3,2,1? If we could, that might lead to a good place.

I think I need to save a result and then overwrite it. That’s not too big a jump. I quickly get this:

function Poly:at(x)
    local result
    result = self.args[1]
    if self.args[2] then
        result = self.args[1]*x + self.args[2]
    end
    if self.args[3] then
        result = self.args[1]*x*x + self.args[2]*x + self.args[3]
    end
    return result
end

The tests run, so this is OK. I’m sort of seeing a pattern. It’s obscured by the scripts going 1; 1,2; 1,2,3.

I get a weird idea. What if I had a local variable telling me which script to check for existing. Like this:

function Poly:at(x)
    local result
    local ck = 1
    result = self.args[ck]
    ck = ck + 1
    if self.args[ck] then
        result = self.args[ck-1]*x + self.args[ck]
    end
    ck = ck + 1
    if self.args[ck] then
        result = self.args[ck-2]*x*x + self.args[ck-1]*x + self.args[ck]
    end
    return result
end

What I’m doing is trying to get the calculations to look similar. And now I can almost see it happening. Each if statement checks to see if it has a coefficient, and if it does, calculates the same as the previous one did, times x, and then adds in its coefficient. Let’s see if we can make that more clear:

function Poly:at(x)
    local result
    local ck = 1
    result = self.args[ck]
    ck = ck + 1
    if self.args[ck] then
        result = result*x + self.args[ck]
    end
    ck = ck + 1
    if self.args[ck] then
        result = result*x + self.args[ck]
    end
    return result
end

Tests are green.

It was still difficult to recognize the duplication in the calcs but all I really did above was remove duplication. Now I’m going to add some duplication, dealing with the setup, which is different from my second and third:

function Poly:at(x)
    local result = 0
    local ck = 1
    if self.args[ck] then
        result = self.args[ck]
    end
    ck = ck + 1
    if self.args[ck] then
        result = result*x + self.args[ck]
    end
    ck = ck + 1
    if self.args[ck] then
        result = result*x + self.args[ck]
    end
    return result
end

Still green. Let’s add even more duplication:

function Poly:at(x)
    local result = 0
    local ck = 1
    if self.args[ck] then
        result = result*x + self.args[ck]
    end
    ck = ck + 1
    if self.args[ck] then
        result = result*x + self.args[ck]
    end
    ck = ck + 1
    if self.args[ck] then
        result = result*x + self.args[ck]
    end
    return result
end

Still green. Let’s remove that common expression …

function Poly:step(ck, x, result)
    if self.args[ck] then
        return result*x + self.args[ck]
    else
        return result
    end
end

function Poly:at(x)
    local result = 0
    local ck = 1
    result = self:step(ck,x,result)
    ck = ck + 1
    result = self:step(ck,x,result)
    ck = ck + 1
    result = self:step(ck,x,result)
    return result
end

Hm. Tests are still green. This is beginning to look like a loop, isn’t it? But what if we were to pass in the arg instead of look it up inside step:

function Poly:at(x)
    local result = 0
    local ck = 1
    result = self:step(self.args[ck],x,result)
    ck = ck + 1
    result = self:step(self.args[ck],x,result)
    ck = ck + 1
    result = self:step(self.args[ck],x,result)
    return result
end

function Poly:step(ckval, x, result)
    if ckval then
        return result*x + ckval
    else
        return result
    end
end

Tests still green. Now let’s make this thing into a loop. I think we can just loop over the args once we set up result. I’ll do it in multiple steps, for reasons I’ll discuss below. First this:

function Poly:at(x)
    local result = 0
    for ck = 1, 3 do
        result = self:step(self.args[ck],x,result)
    end
    return result
end

First time I did this, I left all three double lines, the ck increment and the call to step, inside the loop. I wasn’t trying to make silly mistakes but I wasn’t concentrating either. My tests exploded. I reverted and did it again, this time noticing the ck = ck + 1 and removing it.

But, trust me, I really did this, I left the three calls to step still inside the loop. It exploded again. I reverted again.

Then I did it right. Those were honest, admittedly careless mistakes. The tests saved me.

Now I want to loop over the array with pairs. First, I just use the array index:

function Poly:at(x)
    local result = 0
    for ck, ckval in ipairs(self.args) do
        result = self:step(self.args[ck],x,result)
    end
    return result
end

Green: Now of course it’s clear that we can just pass in ckval:

function Poly:at(x)
    local result = 0
    for _ck, ckval in ipairs(self.args) do
        result = self:step(ckval,x,result)
    end
    return result
end

I renamed the array index to ensure that I wasn’t still using it anywhere. If I had been it would have surely broken a test.

Now, because we’re looping over the table, we know that we never will pass a nil into the step. So we change this:

function Poly:step(ckval, x, result)
    if ckval then
        return result*x + ckval
    else
        return result
    end
end

To this:

function Poly:step(ckval, x, result)
    return result*x + ckval
end

Green. Now step is so simple that we should inline it:

function Poly:at(x)
    local result = 0
    for _, ckval in ipairs(self.args) do
        result = result*x + ckval
    end
    return result
end

And we’re still green. And now I’m very sure that this Poly will work for any set of coefficients the polynomial person wants to give us. We might write one more test just to be sure, but unless it would make me or the other mob members happy, I don’t think I would. I think we’ve driven out a complete and clean implementation with the tests we have.

But we’re not here to talk about building and testing polynomials, we’re here to talk about TDD and what it does for us.

Once we had our polynomials of degree zero, one and two working, we refactored extensively:

  • We changed our interpretation of the input from individual parameters to an array.
  • We pushed functionality around, reversed the order of if statements, created and used subordinate functions, identified common expressions in the text and in time.
  • We modified our open form to look more consistent from step to step, until all the steps looked alike.
  • We pulled out that common element, which helped us to see that what we had was an unrolled loop.
  • We rolled the loop back into a loop.
  • We simplified the function we had pulled out, and found that it was now so simple that it should be inlined back.
  • And at the point we added the loop, we suddenly had a Poly at function that was both simpler than the one we started with, and more general, in that it will handle a polynomial of any given degree.
  • Then we improved it even further.

All along the way, I made mistakes. I’ve surely forgotten some of them, for which I apologize. I was trying to tell a story and forgot that catching mistakes was part of the point of the story. But whether I’ve remembered here to mention them or not, the tests found them all.

All of this, a very complete redesign of Poly, was done while requiring no changes to our tests.

And that’s the real story of TDD. TDD is “red, green, refactor”, and it lets us put in better more general code, or new capabilities, while giving us the confidence that we haven’t broken anything.

This example was just a simple polynomial that went through probably ten or more iterations of improvement, in a morning. But whatever my product, and however many mornings it may take to build and improve its code, my TDD tests both help me shape the original version, and help me be sure that my design improvements are still working.

My first construction was weak and limited. My second was stronger, better designed, and less limited. It could even be even drilled for lightness.

grubby wooden planks before, clean metal solution after

Which means … and this is the punchline, I guess … TDD is the framework that allows me to build incrementally and improve the design as I go, for as long as I go. Without it, I’d be far less certain of design changes, to the point where I’d often be afraid to make them.

And that’s what leads to the messy multiple-case solution that we were accused of up above:

But you could just write a special case chunk of code that just checks for the inputs from the tests and returns the right answer.

TDD doesn’t lead to specialized chunks of code for different cases: it leads away from that kind of flaky design.

Referring back to Strawberries 4, the fact is, when a team is no longer certain of their design, because its interpretive view is too opaque, they can no longer understand the program. And, at the same time, the program’s reactive view doesn’t support changes. So it’s that team that begins to patch in special cases and if statements, in hopes of adding a new capability while not breaking the old ones.

The TDD team maintains its interpretive view and its reactive view longer, which means that it can expand the capability of its behavioral view longer and with more ease.

And that feels good and brings us joy.





Code, for the record:

function testPolynomials()

    _:describe("Polynomials", function()

        _:before(function()
        end)

        _:after(function()
        end)

        _:test("Constant 5", function()
            local poly = Poly(5)
            _:expect(poly:at(1)).is(5)
        end)
        
        _:test("Constant 7", function()
            local poly = Poly(7)
            _:expect(poly:at(3)).is(7)
        end)
        
        _:test("2x+5", function()
            local poly = Poly(2,5)
            _:expect(poly:at(0)).is(5)
            _:expect(poly:at(1)).is(7)
        end)
        
        _:test("3x^2 +5x+7", function()
            local poly = Poly(3,5,7)
            _:expect(poly:at(0)).is(7)
            _:expect(poly:at(2)).is(3*2*2 + 5*2 + 7)
        end)
        
    end)
end

Poly = class()

function Poly:init(...)
    self.args = {...}
end

function Poly:at(x)
    local result = 0
    for _, ckval in ipairs(self.args) do
        result = result*x + ckval
    end
    return result
end