In Part 1 we took a look at how to get our Monty-Hall simulator into a test harness so we could start refactoring to add N doors functionality. In this part, we’ll look at how we can refactor and write test cases to make our code more maintainable while making new functionality easy to add.
Identifying places that need to change
Looking at our current code, we need to change a few places to introduce the new functionality:
- In the game object, doors is hard coded to 3 door objects. We want to make this configurable on initialization.
- There are a lot of calls to getRandomInt which are limited from 0 to 3. This should be 0 to doors.length
- determineContents and switchDoor both have lots of hard coding, we’ll need to add tests to these methods and then come close to re-writing them.
- There is repeated behavior in switchDoor and stay
- resetAll needs to operate on more than 3 doors
- Our UI is dependent on 3 doors (it’s hard coded)
Starting with the game state and doors
First, let’s look at the global game object that manages state for our application. We’re going to leave this in — even though using a global variable isn’t a great practice — since the rest of the application relies on it. We do however want to add a method that can initialize game with an appropriate number of doors. We’ll call this method in our characterization tests with 3 doors to ensure that we have the same behavior.
After running our characterization tests to confirm that the game still works as expected, we can safely change all calls to getRandomInt to limit from 0 to the number of doors available.
We’ve also extracted the hard coded number 3 to a DOORS variable.
determineContents is a nice method to unit test because it doesn’t involve the UI. We’ll start writing our first unit test by working on this method. The first important thing to realize about determineContents is that it operates on a global game object. This makes it harder to write unit tests for because we’ll be operating on global state in the tests and we’ll have to be very careful about resetting that before and after our tests run.
Luckily there’s an easy change we can make that’s relatively safe and makes our tests easier to write. Let’s make determineContents take a game object as an argument instead of operating on global state. We can then change all call sites to pass in the global game object and our characterization tests should still pass.
This is an example of a seam (more specifically you might say this is a variable seam) from Working Effectively with Legacy Code. The seam is the use of the global variable game and the enabling point is the method argument. We can make use of this seam safely because
- The change is relatively simple and syntactic, we are just passing something new into the method which requires making the same change at all call sites
- Our characterization tests support this change
And now we can write unit tests around determine contents
Note that the unit tests we write initially are characterization tests. They verify the current behavior of this method. After we have our tests in place, we can write a new failing test that requires the new behavior. Then we implement that required behavior.
Our tests pass and we can verify everything is working with a quick manual test if we wish.
We have started to make real progress implementing our feature. We have one method covered with unit tests and some initial compatibility with N doors but we have a long way to go. Next time we’ll look at making the rest of the required changes before separating out the UI code for manipulating elements.