A QUICK EXAMPLE
Let's take a peek into the development of the project from later in the book. We have a Movie class which now needs to accept multiple ratings (e.g., 3 as in "3 stars out of 5") and give access to the average.
As we go through the example, we will be alluding to a metaphor for the TDD flow originated by William Wake: The TDD Traffic Light[URL 9][URL 61].
We start by writing a test, and we start the test by making an assertion that we want to be true:
public void testRating() { assertEquals("Bad average rating.",4,starWars.getAverageRating()); }
Now we need to set the stage for that assertion to be true. To do that we'll add some rating to the Movie:
public void testRating() { starWars.addRating(3); starWars.addRating(5); assertEquals("Bad average rating.",4,starWars.getAverageRating()); }
Finally, we need to create the Movie instance we are working with:
public void testRating() { Movie starWars = new Movie("Star Wars"); starWars.addRating(3); starWars.addRating(5); assertEquals("Bad average rating.",4,starWars.getAverageRating()); }
When we compile this, the compiler complains that addRating(int) and getAverageRating() are undefined. This is our yellow light. Now we make it compile by adding the following code to Movie:
public void addRating(int newRating) { }
public int getAverageRating() { return 0; }
Note that since we are using Java, we must provide a return value for getAverageRating() since we've said it returns an int.
Now it compiles, but the test fails. This is the red light (aka red bar). This term is derived by the JUnit interfaces that present a progress bar that advances as tests are run. As long as all tests pass, the bar is green. As soon as a test fails, the bar turns red and remains red. The message we get is:
Bad average rating. expected:<4> but was:<0>
Now we have to make the test pass. We add code to getAverageRating() to make the test pass:
public int getAverageRating() { return 4; }
Recompile and rerun the test. Green light! Now we refactor to remove the duplication and other smells that we introduced when we made the test pass.
You're probably thinking "Duplication.. . what duplication?" It's not always obvious at first. We'll start by looking for constants that we used in making the test work. Sure enough, look at getAverageRating().It returns a constant. Remember that we set the test up to get the desired result. How did we do that? In this case we gave the movie two ratings: 3 and 5. The average result is the 4 that we are returning. So, that 4 is duplicated. We provide the information required to compute it, as well as returning it as a constant. Returning a constant when we can compute its value is a form of duplication. Let's get rid of it.
Our first step is to rewrite that constant into something related to the provided information:
public int getAverageRating() { return (3 + 5) / 2; }
Compile and run the tests. We're OK. We have the courage to continue. The 3 and 5 are duplicate with the arguments to addRating() so let's capture them. Since we add the constants we can simply accumulate the arguments. First we add a variable to accumulate them:
private int totalRating = 0;
Then we add some code to addRating():
public void addRating(int newRating) { totalRating += newRating; }
Now we use it in getAverageRating():
public int getAverageRating() { return totalRating / 2; }
Compile, test, it works! We're not finished yet, though. While we were refactoring we introduced another constant: the 2 in getAverageRating().The duplication here is a little subtler. The 2 is the number ratings we added, i.e., the number of times addRating() was called. We need to keep track of that in order to get rid of the 2.
Like before, start by defining a place for it:
private int numberOfRatings = 0;
Compile, run the tests, green. Now, increment it every time addRating() is called:
public void addRating(int newRating) { totalRating += newRating; numberOfRatings++; }
Compile, run the tests, green. OK, finally we replace the constant 2 with numberOfRatings:
public int getAverageRating() { return totalRating / numberOfRatings; }
Compile, run the tests, green. OK, we're done. If we want to reinforce our confidence in what we did, we can add more calls to addRating() and check against the appropriate expected average. For example:
public void testLotsOfRatings() { Moviegodzilla = new Movie("Godzilla"); godzilla.addRating(1); godzilla.addRating(5); godzilla.addRating(1); godzilla.addRating(2); assertEquals("Bad average rating.",2,godzilla.getAverageRating()); }
I need to underline the fact that I recompiled and ran the tests after each little change above. This cannot be stressed enough. Running tests after each small change gives us confidence and reassurance. The result is that we have courage to continue, one little step at a time. If at any point a test failed we know exactly what change caused the failure: the last one. We back it out and rerun the tests. The tests should pass again. Now we can try again... with courage.
The above example shows one school of thought when it comes to cleaning up code. In it we worked to get rid of the duplication that was embodied in the constant. Another school of thought would leave the constant 4 in place and write another test that added different ratings, and a different number of them. This second test would be designed to require a different returned average. This would force us to refactor and generalize in order to get the test to pass.
Which approach should you take? It really depends on how comfortable you are with what you are attempting. Remember that you do have the test to safeguard you. As long as the test runs, you know that you haven't broken anything. In either case you will want to write that second test: either to drive the generalization, or to verify it.
|
No comments:
Post a Comment