Demo Goals
So far we only have test output to show for all of our hard work. Wouldn’t it be nice if we could demonstrate our system with output? In reality, a product owner would probably require such a demo, so here’s a list of things we want to accomplish with our demo:
- Be able to create 40 locations with the correct name
- Be able to show players taking turns and moving around the board
- Show things happening to the players (e.g. passing Go or hitting Luxury Tax)
Along the way we’ll introduce a light-weight use of the boost library as well to handle memory deallocation.
The Back 40
First let’s create a simple set of 40 locations based on a file. Here’s an example of what that file might look like:
type | name |
go | Go |
location | Mediterranean Avenue |
location | Community Chest |
location | Baltic Avenue |
it | Income Tax |
We need something that will construct the correct kind of Location based on the first column and give it the name in the second column. We also need to make sure to connect each of the locations to each other and form a circle.
Of course, we’ll test our way into this.
To accomplish all of this, we’re going to use the Boost C++ library and the C++ standard library. Here are a few example building blocks: C++ Monopoly Building Blocks.
Here we go…
Red: Our First Test
Here’s our first goal, create a “board” of one location. We’ll use for our data set the following:
type | name |
location | LocationName |
Here’s our test (in a new test file):
BoardBuilderTest.hpp
Here’s a breakdown:
Line | Description |
15 | I do not want to actually use a real file and deal with file I/O so I’m going to use a string as my source of data. |
16 | the C++ standard library offers the class istringstream that allows us to create an istream from a string. This allows my unit under test to read from a “file” that’s in memory. |
18 | I’m calling a class method (static method) called buildBoard on the class BoardBuilder. This method takes a stream representing our locations and returns back the first location listed in the file. Remember that our locations form a circular list so having just the “first” location gives us all of them. This method is performing dynamic memory allocation so rather than return a standard pointer, I return a shared pointer that automatically will cause the underlying memory to get deleted when I return from this method. This is not a perfect solution, but we’re working our way there. |
19 | First I want to verify that the location I got back has an expected name (this is a new method we’ll need to add to Location. |
20 | Next, I want to make sure that the list is circular. It’s size is one, so the next of start should be itself. |
Red: Get it to compile
We have to extend our location class and we have to create a BoardBilder class. Here they are:
Location.hpp
Note: We’ve already seen this pattern repeated. Add a method get an attribute that returns a constant value. Then get the test to compile then go back and update to use a real reference. We’re going to skip this step and go right to supporting a name attribute. Add the following method and attribute:
Now we need to create the BoardBuilderClass. Here’s the header file:
BoardBuilder.hpp
Next, we need to get our code to compile, so we’ll need to add a definition for the buildBoard method:
BuildBoard.cpp
Build your system and verify that it compiles. You test should fail.
Green: Get your test to pass
We’ve already updated location with a new attribute, name. We need to fill out our definition of the buildBoard method and add any additional missing pieces as well:
BuildBoard.cpp
Here’s the breakdown:
Line | Description |
7 | Define a tokenizer from the boost library that will use a character separator to split strings (lines) into individual tokens |
9 - 10 | Use the non-member function getline which reads from an istream into a std::string. This reads up to a new line or end of file. |
12 | Define our separator character to be a tab. |
13 | Create a tokenizer for the line we read from the istream, splitting the line by tab chracters, as defined by our char_separator. |
16 | *it gives us the first token (always a string) and then the ++ moves the iterator to the next token. |
17 | Read the second token, the name of the location. |
19 | Create our shared pointer holding onto a new location created using a new constructor, which we’ll have to add to the location class. |
20 | Connect the location to itself to make the list circular. |
Notice that there’s quite a bit of hard-coding going on here. That’s OK, we’re going to use a series of tests to incrementally improved and refactor this code.
We have to add a constructor to location that takes in a name. Since Locations are currently constructed with a no-argument constructor in our tests, can simply update the constructor:
Build and run, are you green?
Refactor
We do not have a lot of duplicate code, but we do have code that might seem incomplete. There’s not much to refactor yet, but that’s coming up.
Now is a great time to check in your work.
Red: Create two locations in a circle
|type|name| |location|Location1| |location|Location2|
We want to create two locations in a circle. Here’s a test:
This test is similar to the first test.
- We make sure there are two locations with two names (lines 7 and 8),
- On line 9 we make sure that the location returned does not point to itself
- We then make sure that the locations for a circle on line 10.
Red: Get it to compile
This test already compiles but it does not pass. The fact that is does not pass suggests that it might be testing something useful. The fact that we did not have to add any code to get it to compile could mean we’re testing an assumption or this test covers the same thing as another test. We’ll review that in the refactor step.
Green: Get it to pass
We need to update the buildBoard method:
This is quite a jump. That’s often the case going from dealing with 1 thing to dealing with many things, as we did in this case.
Run your tests and make sure you’re green.
Refactor: One of these tests subsumes the other
Notice that the second test in BoardBuilderTest.hpp does everything that the first test does and a little more? It turns out that after running the tests, my first test failed because I “primed the pump” by reading the first location and then going into the loop but then I additionally use a do-while loop instead of a while loop. The test caught this problem and then I fixed the underlying code.
So you might consider getting rid of this test but I’d recommend leaving it in to verify that any changes to that test don’t break handling a single location in a loop.
Red: Time To Create Different Types
OK, now we need to make sure that while the board is building locations, it can do so with different types. For example:
type | name |
go | Go |
location | l1 |
it | Income Tax |
This describes a board with three locations and three different kinds of locations (based on the first column).
Here’s a test:
Note: You’ll need to make sure to #include “Go.hpp” and “IncomeTax.hpp”
Red: Get it to compile
This code compiles, it just does not pass so we can go right to Green.
Green: Get it to pass
We need to update the BoardBuilder:
Note: To get this to compile you’ll need to include “Go.hpp” and “IncomeTax.hpp”. You’ll also additionally need to add a constructor to Go and IncomeTax that take a parameter:
Go.hpp
IncomeTax.hpp
Verify that your tests pass.
Refactor
Here are a couple of things to notice:
- Every time we add a new kind of location we have to update board builder. While this isn’t awful, it’s going to get ugly.
- BoardBuilder is good at reading the file and connecting up locations, how about we factor our the construction of different kinds of location into a simple Location factory:
LocationFactory.hpp
LocationFactory.cpp
And we need to update BoardBuilder.cpp:
BoardBuilder.hpp
Verify you’re still green.
Now is a great time to checkin.
Refactor: Test Smells
Is it strange that we have a test in BoardBuilder that tests we’re creating the correct kinds of locations? Since that is not a responsibility of BoardBuilder but LocationFactory, we should move the test and add missing tests to make sure that LocationBuilder creates every kind of location;
LocationFactoryTest.hpp
Here is a series of tests to make sure we can create each kind of Location there is:
To make this work, we need to update our LocationFactory:
LocationFactory.cpp
The tests all pass, so now is a good time to checkin.
Building Go To Jail Correctly
Right now we have a location, Go To Jail, that has as an attribute that is the destination location for when people land on it. We have a factory responsible for constructing the different kinds of locations but it does not currently keep track of all of the locations.
Let’s add a test to verify that when we get back an instance of GoToJail, it’s destination location is properly set:
Red: Write a test
A bit of a brute force test, but it gets the job done. Construct a board with 14 locations, one is known as Just Visiting while the other, Go To Jail, uses Just Visiting as its destination.
Red: Get it to compile
This test compiles as necessary but it does not pass. This might indicate we’re refining the underlying implementation to assert more assumptions about building all of the locations.
Green: Get it to pass
Since we’ve already simplified BoardBuilder and taken out its responsibility for building the actual correct kinds of locations, the next change will end up going into LocationFactory:
LocationFactory.cpp
The big change is that before the factory was stateless, it now keeps track of everything it constructors. This allows later locations build to make reference to previous locations by their names. If you review the section of code that checks the whether the type is “gotojail”, you’ll notice that it expects an additional token no the line, the name of its destination location. We look it up and set the value just after creating it.
Verify that your changes pass successfully.
Check in.
Comments