100 Final Simple Example
100 Final Simple Example
Test Driven Development (TDD) is easy to describe: 1. 2. 3. 4. Write a failing test, Make the test run, Refactor (clean up) the code so the design is cleaner, and Repeat for the next test.
You build your working software up small incremental steps, one test at a time. This cuts down hugely on the amount of time required to rework code to fix defects and time can, instead, be used to refactor the code, keeping the code-base clean, well designed and malleable, so it is easy and cheap to work with. Significantly Less rework + significantly easier code = Significantly higher productivity. Even better, the tests act as a safety net catching your mistakes quickly so that they are cheaper and easier to fix. That's an easy process to describe, but that's not enough: its only when you do it that you truly get it. How do you try TDD, for real, if youre not a programmer? How do you persuade your boss to let you do TDD, if he or she cant truly get it? Here's how. This site runs through a very simple TDD example converting integers into their roman number equivalent - that you or your boss can do easily using Microsoft Excel and Visual Basic for Applications (VBA). Its not intended to teach how to do TDD or how to program, but rather its a hands on example where you can get a feel for the process and the possibilities.
All you need is Excel and an open mind. It will help if youve had a little programming experience but not much.
You add test data into a spreadsheet (1=i, 2=ii, 3=iii, etc) one test at a time. You write a new Excel function using Visual Basic for Applications (VBA) to make each test work, as it is added. You set up conditional green and red formatting in the spreadsheet to tell you quickly and visually when the tests are passing and failing.
Now lets create your first test and setup your test environment.
I type column headings in row 1 (Integer in Column A and Roman in column B). I add my first row of test data in row 2, using the test case that 1=i.
Then I add another heading into cell C1 called =i2r() and I use in cell C2 I type the formula =i2r(A2). This is the function that Im going to build, test-case by testcase.
But, since the function i2r() doesnt exist yet, Excel gives an error. This is the same as a compile error in, say, Java.
I need to add a new function called i2r() before it will work. So I chose Tools/Macros/Visual Basic Editor which, unsurprisingly, opened up the Visual Basic editor. I could also have pressed ALT-F11. I then hunt around the menus and find INSERT/PROCEDURE which sounded handy I presume that this is how I insert a new function (not just a procedure). Bad news: its greyed out. But, just underneath it is INSERT/MODULE which I bravely click and it gives me a new module. I check under the INSERT menu and PROCEDURE is now enabled. So I click on it and add in a new function (not procedure) naming it i2r().
I switch back to my spreadsheet and manually recalculate the spreadsheet by pressing F9. It shows me:
My first test is now failing! This is progress. But, why is i2r returning 0 ? I look at the code and realise the VBA default must be to return 0.
I switch to VBA, take a peek in the help files to learn how VBA returns parameters and then change the function so that it returns the string I. I also add in Application.Volatile to the top of the function which forces Excel to recalculate spreadsheet cells which use VBA code that has changed (it doesnt do this automatically otherwise). You wont believe how well this little trick is hidden in the Excel help files.
So I pop back to Excel. Do a manual F9 recalc and hey-presto! my first test has passed.
Phewww! Since Im writing up my steps as I go this has taken me a while, but in normal circumstances I reckon it would take 1-2 minutes, tops. Right, now let's add some Visual Feedback - I want to know when my tests are passing and failing.
Visual Feedback
I want to use column D as my automated testing column. I want it to show Green if the test works and Red (with white ink) if it fails. That way the spreadsheet should shout out at me if tests are failing.
So I add another heading Pass/Fail and add the formula =IF(B2=C2,"pass","fail") into cell D2. I quickly overtype cell C2 with i to check it works with a pass and it does. Then I put in the word blah to check that the fail works and it does too. I undo both of those and play around with Excels conditional formatting feature and make true show in Green background and false show in Red Background.
That's right! A passing test! In green. Just like traffic lights! Yippee!
Second Test
I figure now is as good a time as any to try my second test.
I add in the test 2 = II and copy the C and D formulas from the existing row
So, now I have my 2nd test and it is failing. It might not feel like it, but this is good. Very good! Its the rhythm of TDD write a failing test, make it work using the simplest way possible, then refactor (i.e. tidy up) the code.
After a quick peek in the Excel help files on the if statement, I change the code
Public Function i2r(i As Integer) As String application.volatile If i = 1 Then i2r = "I Else i2r = "II End If End Function
Two passing tests! Im on a roll. Now, let's add some more tests.
Tests 3 to 8
I take a look at the code and wonder if I can tidy it up at all (refactor) and I cant see anything[1]. So I add in the next test: 3 = II, it fails, I change the code so it passes, but I still cant see anything to refactor. I repeat this process for tests 4, 5, 6, 7, 8, adding each test one at a time making it work, trying to refactor. I start to think that this could be a long and drawn out process. I cant see anything that I can easily refactor.
If i = 1 Then i2r = "I" ElseIf i = 2 Then i2r = "II" ElseIf i = 3 Then i2r = "III" ElseIf i = 4 Then i2r = "IV" ElseIf i = 5 Then i2r = "V" ElseIf i = 6 Then i2r = "VI" ElseIf i = 7 Then i2r = "VII" ElseIf i = 8 Then i2r = "VIII" End If
End Function
My first "refactoring"
Do you think the code looks a little ugly?
If i = 1 Then i2r = "I" ElseIf i = 2 Then i2r = "II" ElseIf i = 3 Then i2r = "III" ElseIf i = 4 Then i2r = "IV" ElseIf i = 5 Then i2r = "V" ElseIf i = 6 Then i2r = "VI" ElseIf i = 7 Then i2r = "VII" ElseIf i = 8 Then i2r = "VIII" End If
End Function
I see a pattern with the 1, 2 and 3 and 6, 7, 8. I wonder if I can do something with the repeating Is.
10
When I consider the code, I see that there are two ways in which I think I can tidy up the code (refactor). First, I could turn 1, 2 and 3 into a loop. Second, I could turn 6, 7 and 8 into (5 + 1, 2 or 3). I think Im clever enough to do both at the same time but then I recall the XP idea of doing things one at a time, since its more likely to work/ less likely to break. I flip an imaginary coin and decide to turn 1, 2 and 3 into a loop.
First, Im not sure quite why, but I move the code for 1, 2 and 3 down to the bottom of the code.
11
If i = 4 Then i2r = "IV" ElseIf i = 5 Then i2r = "V" ElseIf i = 6 Then i2r = "VI" ElseIf i = 7 Then i2r = "VII" ElseIf i = 8 Then i2r = "VIII" End If
If i = 1 Then i2r = "I" ElseIf i = 2 Then i2r = "II" ElseIf i = 3 Then i2r = "III" End If
End Function
Just to make sure this hasnt broken anything I switch back to the spreadsheet and recalc. I probably didnt need to do this but it has been a long time since Ive coded and Im a bit nervous doing this. All of the tests still work.
12
1 2 3 4 5 6 7 8 9
A Integer 1 2 3 4 5 6 7 8
13
If i = 4 Then i2r = "IV" ElseIf i = 5 Then i2r = "V" ElseIf i = 6 Then i2r = "VI" ElseIf i = 7 Then i2r = "VII" ElseIf i = 8 Then i2r = "VIII" End If
i2r = "" While i < 3 And i > 0 i2r = i2r + "I" i = i - 1 End
End Function
I recalc the spreadsheet and it doesnt work. I realise this when I look at the code and notice I put a END, rather than a WEND, to close off the While loop.
So, I pop back to the spreadsheet and press F9 to rerun the tests again.
14
1 2 3 4 5 6 7 8 9
A Integer 1 2 3 4 5 6 7 8
C i2r I II
When I look back at the code I realise that Ive done two things wrong.
First, the loop should be <= 3, not < 3. I change that, recalc the tests and 3 is fixed.
But what about 4 8? Theyre still broken. And, no wonder, the i2r = before the loop destroys them.
I fix the code for the current tests by moving i2r= to the top of the code.
When I recalc the spreadsheet all of the tests work! The tests are very comforting a safety net.
15
i2r = ""
If i = 4 Then i2r = "IV" ElseIf i = 5 Then i2r = "V" ElseIf i = 6 Then i2r = "VI" ElseIf i = 7 Then i2r = "VII" ElseIf i = 8 Then i2r = "VIII" End If
End Function
16
So. The 2nd Refactoring. Hmmm. I want to change the code so that 6, 7, and 8 are 5 + 1, 2, or 3.
6, 7, and 8 are 5 + 1, 2, or 3
I want to change the code so that 6, 7, and 8 are 5 + 1, 2, or 3.
I have a wee discussion with myself before making any changes and decide to comment out the 6, 7 and 8 elseifs, change the if i = 5 to if i>=5, and subtract 5 from the i2r, and see how it works. I make these changes and break a lot of the tests. Hmmm. What to do?
I turn on the debugger. And the moment I step through the very simple code I see a blindingly obvious keying error that I could have found without the debugger if only Id looked at the code a little harder. I fix my mistake. I turn off the debugger. I switch to Excel and recalc. All of the tests pass. I remove the code that Id commented-out.
17
i2r = ""
End Function
I look at the code and Im happy with it, but not thrilled, so I look to see if I could refactor it a bit more. But, you know what? I don't feel confident enough to do that so, instead, I decide to add a few test cases to understand the problem better.
18
In other words, I want to do a bit more analysis and I am going to do it with the code and tests.
Tests 9, 10 and 11
I add the test 9 = IX. It fails. I make it work by adding another ifelse, just like the 4. Cant see any way to refactor it. I add the test 10 = X. It fails. Again I add it as another ifelse, just like the 4 and 9.
i2r = ""
If i = 4 Then i2r = "IV" ElseIf i = 9 Then i2r = "IX" ElseIf i = 10 Then i2r = "X" ElseIf i >= 5 Then i2r = "V" i = i - 5 End If
19
i = i - 1 Wend
End Function
As I add the test 11 = XI, I notice the i following the x and I think about the 12 and 13. This looks like its the good old repeating I situation.
Im tempted to add the 12 and 13 test cases in too, but I decided not to. Why? After all, it seems easy enough to do, but then Id move into trying to make 3 tests work at once and although theyd probably work I suspect that its not the intention of TDD. And besides, it is easy enough to add the 12 and 13 in afterwards.
20
i2r = ""
If i = 4 Then i2r = "IV" ElseIf i = 9 Then i2r = "IX" ElseIf i >= 10 Then i2r = "X" i = i - 10 ElseIf i >= 5 Then i2r = "V" i = i - 5 End If
End Function
21
Do you want to see what happens with Tests 12 -15? I break the code.
Tests 12 -15
Here's the code as it currently stands:
Public Function i2r(i As Integer) As String Application.Volatile
i2r = ""
If i = 4 Then i2r = "IV" ElseIf i = 9 Then i2r = "IX" ElseIf i >= 10 Then i2r = "X" i = i - 10 ElseIf i >= 5 Then i2r = "V" i = i - 5 End If
22
Wend
End Function
I look to see if I can refactor anything. I cant see anything. Hmmm ... I add in the 12 = XII test case. It works straight away. As does the 13 = XIII. I add in the 14 = XIV test case. Now the easiest way to make this test work is to put in an if i = 14 and I do this. But when I go to refactor I realise that its really just 10 + 4, so I move the if i =4 out of the string of ifs and move it after the 5 and between the 3 , 2 and 1 code. It should just trickle through. My tests still all run after making the change.
i2r = ""
If i = 9 Then i2r = "IX" ElseIf i >= 10 Then i2r = "X" i = i - 10 ElseIf i >= 5 Then i2r = "V" i = i - 5 End If
23
End Function
I look for ways to refactoring but I (with my limited coding ability) cant see any that I can do with confidence.
I add the 15 = XV test case. Im starting to see a pattern here and I change the code easily.
i2r = ""
If i >= 5 Then
24
End Function
But 15 doesnt work and the test for 9 breaks. Let me show you.
1 2 3 4 5 6
A Integer 1 2 3 4 5
B Roman I II III IV V
C i2r I II III IV V
25
7 8 9 10 11 12 13 14 15 16
6 7 8 9 10 11 12 13 14 15
pass pass pass fail pass pass pass pass pass fail
I suspect if youve been following along in Excel then youve discovered you too make lots of little mistakes as you go. At least I hope so and that it isnt just me. It is one of the nice things about all those tests when you break something by making a simple mistake, the tests tell you, you fix it quickly and cheaply, and move on.
When I see that the 9 test case is also broken it is immediately obvious what Ive done wrong. I fix it quickly. The tests work.
i2r = ""
26
End If
End Function
Then I do a bit of tidying up moving things around a little. For each small change I make, I recalc the tests.
i2r = ""
27
i = i - 10 End If
End Function
28
i2r = ""
29
End Function
i2r = ""
30
End Function
I try, but fail, to add a function which I can use to move all that repeating code ... but I've not coded for a long time and I fail. Out of laziness and lack of imagination I give up and decide to add more test cases first. I add a test case for 16 =XVI and then I get enthusiastic and add the tests into the spreadsheets for 17 through to 19. I think they should work straight away and they do. I add 20 = XX and it doesnt work immediately. I look at the code and quickly change the IF i >= 10 to a while loop.
Hmmmm, I think. I reckon that these tests should now work all the way to 50.
So I add in the integer values from 21 30 and instead of filling in the roman numerals, I cheat a little and copy down the i2r() function and eyeball them to check that theyre right. They all are so I copy the values from my function into the test data. Then I chose a few other numbers up to 50, which I figure will fail with the current code. But, when I type in 46 I realise that the 40s are a special case like 4 and 9, and later on 90, 400 and 900. So I check google to find out what the symbol for 40 is its XL. I
31
add in the test case for 40 and make it work. I add in a test case for 49 and it works. Im confident that the rest of the 40s will work too. I add in a test case for 50 = l and as expected it doesnt work. By now, the code is trivial so I make it work. Then I think about doing some more Refactoring.
i2r = ""
32
Wend
End Function
I figure that I want to create a function where I feed in each roman numeral and if the current value of i > the decimal equivalent of the roman numeral then concatenate the roman numeral on the end of i2r and subtract the decimal equivalent from i.
33
The only thing that is stopping me is the mixture of whiles and ifs in the pattern above. The Is and Xs are whiles, but the rest are ifs. Hmmm, I think. After a bit of contemplation Im pretty sure that each of the ifs can simply be replaced with a while.
But, how can I be sure? The easiest way to be sure is to try it and see. I have the tests to save me if I make a mistake and I have undo to roll back if I need to. But, to be on the safe side I decide to try this from the bottom up. That is, I start with the Is.
This is what I come up with, for the Is (I couldnt think of a good function name, so m it is):
i2r = i2r + m(i, "I", 1) 'While i >= 1 ' ' i2r = i2r + "I" i = i - 1
'Wend
End Function
Public Function m(ByRef i As Integer, ByVal RomanCharacter As String, ByVal IntegerValue As Integer) As String While i >= IntegerValue m = m + RomanCharacter i = i - IntegerValue Wend End Function
And it works.
34
So I try it on the 4 IF, replacing the if i = 4 with i2r = i2r + m(i, "IV", 4) And it works. So I try it on the 5. It works too. So, I do all of the individual roman numerals. Recalculating the tests after each small change.
i2r = ""
i2r = i2r + m(i, "L", 50) i2r = i2r + m(i, "Xl", 40) i2r = i2r + m(i, "X", 10) i2r = i2r + m(i, "IX", 9) i2r = i2r + m(i, "V", 5) i2r = i2r + m(i, "IV", 4) i2r = i2r + m(i, "I", 1)
End Function
35
Integer) As String While i >= IntegerValue m = m + RomanCharacter i = i - IntegerValue Wend End Function
i2r = m(i, "L", 50) + m(i, "XL", 40) + m(i, "X", 10) + m(i, "IX", 9) + m(i, "V", 5) + m(i, "IV", 4) + m(i, "I", 1)
End Function
Im confident that I understand how this function is supposed to work so I quickly add in the 90 = XC , 100 = C , 400= CD, 500 = D, 900 = CM and 1000 = M code, their test cases, and a healthy smattering of tests in between. When I get to 1000 = M I realised that my laziness with the function name m() was very sloppy and I change it. The variable named i wasnt very good
36
37