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.
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.
1 test_places_X_on_users_position: 2 push_board 3 4 mov eax, esp ; A pointer to the board 5 call perform_turn 6 7 get_token esp, 0x0 8 assert_equal eax, x_token 9 10 pop_board 11 ret
I then wrote the first implementation of
perform_turn which makes a call to
1 perform_turn: 2 push eax ; The pointer to the board 3 4 call get_user_move 5 set_space [esp], eax, x_token ; The square brackets mean 6 ; esp is being dereferenced 7 8 pop eax 9 ret
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.
1 test_retries_if_user_enters_invalid_token: 2 push_board 3 4 mov eax, esp ; A pointer to the board 5 mov ebx, canned_user_response ; A pointer to a get_user_move 6 mov dword [number_of_tries], 0x0 ; Reset try count 7 call perform_turn 8 9 mov dword eax, [number_of_tries] ; User should be asked twice 10 assert_equal eax, 0x2 11 12 pop_board 13 ret
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.
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.
1 perform_turn: 2 push ebp 3 mov ebp, esp 4 5 push eax ; The board [ebp - 0x4] 6 push ebx ; get_user_move [ebp - 0x8] 7 8 .player_ones_turn ; The dot creates a label that can be 9 call [ebp - 0x8] ; jumped back to 10 is_valid_space [ebp - 0x4], eax 11 jne .player_ones_turn 12 set_space [ebp - 0x4], eax, x_token 13 14 add esp, 0x8 15 pop ebp 16 ret
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.