I tend to be a skeptical person. When I encounter a new idea, I often start on the offense. I try to force the new idea to stand up to scrutiny. My introduction to test-driven development was no different. I was most skeptical of the idea that TDD would result in better design choices. Indeed, I have watched on a few occasions as TDD has led people to focus so hard on the details of the test that they lose sight of the big picture. While I have seen TDD lead people down the wrong path, here I present an experience where it led me down the right path. Ultimately the pressure placed upon my code by the tests led me to a very flexible solution.
The Problem
As an exercise in learning assembly, I set out to write a game of Tic Tac Toe with an unbeatable computer player. I was particularly interested to see what a test-driven approach would look like in such a primitive language.
Beyond some superficial differences, test-driving the main algorithm turned out to be no different than it has been in other languages. The challenge came when testing the main loop, which requires user interaction. Given my unfamiliarity with assembly, the solution didn't immediately jump out at me.
The First Attempt
I wrote the first test to verify that the user's token gets placed on the board correctly.
I then wrote the first implementation of perform_turn
which makes a call to get_user_move
.
Now I had to figure out how to get get_user_move
to return a canned response. At first I tried using the linker. I wrote an implementation of get_user_move
in the test file that simply puts 0 in eax
. When I compile the tests, the canned version of get_user_move
gets linked into perform_turn
. When I compile the real binary, the correct version gets linked in and all is well.
The next test specified that the game should ask the user for a space until it gets a valid one.
I also had to write a new implementation of get_user_move
to increment a counter and return an invalid move the first time it gets called. The second time it gets called, it returns a valid move.
Then I encountered a new problem. Only one version of get_user_move
can be linked into the program at a time, but I needed two different versions for my two different tests.
The Solution
After thinking about this for a while, I realized that I can fall back on the techniques I've used in the past. I decided to inject the get_user_move
dependency into perform_turn
by passing it a function pointer.
Conclusion
The flexibility of the final perform_turn
procedure is the result of bending to meet the different constraints introduced by the tests. Passing around function pointers seems like an obvious technique now, but at the time it was something of a revelation. Not only did this solve my testing problem, but it made perform_turn
more versatile. In the final implementation of perform_turn
, I can pass in a variety of different procedures to fill the role of get_user_move and they can all coexist. Changing the way perform_turn
gets its move from the user is as simple as changing one of its arguments.