Test Driven Assembly

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.

 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 get_user_move.

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.

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.

 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

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.

Michael Baker enjoys Ruby, Clojure, Haskell, coffee, and croissants.