Test Driven Assembly

Test Driven Assembly

Michael Baker
Michael Baker

June 07, 2012

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.


test_places_X_on_users_position:
				push_board

				mov eax, esp ; A pointer to the board
				call perform_turn

				get_token esp, 0x0
				assert_equal eax, x_token

				pop_board
				ret

I then wrote the first implementation of perform_turn which makes a call to get_user_move.


perform_turn:
				push eax ; The pointer to the board

				call get_user_move
				set_space [esp], eax, x_token ; The square brackets mean
																																		; esp is being dereferenced

				pop eax
				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.


test_retries_if_user_enters_invalid_token:
				push_board

				mov eax, esp ; A pointer to the board
				mov ebx, canned_user_response ; A pointer to a get_user_move
				mov dword [number_of_tries], 0x0 ; Reset try count
				call perform_turn

				mov dword eax, [number_of_tries] ; User should be asked twice
				assert_equal eax, 0x2

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

perform_turn:
				push ebp
				mov ebp, esp

				push eax ; The board [ebp - 0x4]
				push ebx ; get_user_move [ebp - 0x8]

				.player_ones_turn ; The dot creates a label that can be
				call [ebp - 0x8] ; jumped back to
				is_valid_space [ebp - 0x4], eax
				jne .player_ones_turn
				set_space [ebp - 0x4], eax, x_token

				add esp, 0x8
				pop ebp
				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.