In Part 3 we did some major refactoring and added new tests. Now that we’ve refactored out everything, we’re prepared to remove our dependence on the UI in our tests, refactor the remaining UI pieces, and implement our feature.
Introducing a method seam
The first step in removing out dependence on the UI is providing a seam that we can override in test cases for document.getElementById. Luckily we have a very simple UI so we only need to stub out this one method. In order to do that, we can introduce a method byId which we’ll override in our test cases to return a mock element object (with some carefully selected fields). In the actual application, byId will defer to document.getElementById
Find and replace through our editor makes this change safe. Next we can stub out the method in our test cases and completely remove the extra invisible elements that we had to add before.
Our tests take a bit longer to run now because of the extra object creation but it’s not a big problem. The important thing is that stubbing out the UI in this way allows us to write tests cases that check the state of the game after methods that interact with the UI.
Now that we have our seam for the UI, we can add a test case to resetAll and then refactor it to support more than 3 doors. First lets add our characterization test.
And then we’ll change the code to iterate over the doors instead of being hard coded to an expected number.
Our tests pass meaning that resetAll is officially compatible with the new feature.
Implementing the feature
By this time, we’ve added test cases to most of our code and refactored it so that making the change is relatively simple. Everything is controlled by the value in the DOORS variable. We need to
- Initialize the game with the proper number of door elements (with the correct ids)
- Expose a way for the user to select one of the doors (1 button per door won’t work for a large number of doors)
- Give the user a way to pick their door when they switch
Since we’re adding a new feature, we can now switch over to TDD and write a test to check that our initialization creates the right number of door elements
We can also remove the hard coded 3 door divs.
The rest of the functionality is quite straightforward to implement and is a bit outside the scope of this post so I’ll just include the final commit here
You’ll note that with large numbers of doors (in the low hundreds) our application is quite slow. We could improve the UI/UX by showing pictures of doors and goats instead of just colored boxes, or by letting the user pick the door they switch to, or any other number of ways. Our final implementation for this example isn’t really the point though. The point of this series is what enabled us to easily make this change for the final commit. At every stage in our journey we were able to keep the app in a usable state while slowly improving the testability and adding functionality. These are the keys to a good refactoring.
- Work with your seams and introduce new ones when necessary
- Refactor safely to keep existing functionality working while keeping the end goal in mind
- Don’t introduce new functionality until you’ve completed your refactoring
- Support your refactoring with tests, and switch to TDD as soon as feasible for new functionality
P.S. As an exercise to the reader, how would we refactor our N doors implementation to support opening every door except 2 (i.e. with N=100, Monty will open 98 doors and then gives us the option to switch or stay). Could we support a switch to toggle between those two rulesets?