The start of the classic bowling game kata, in Java with JUnit For Eclipse 4.5 with Java 1.8
This is the well-known "Bowling Game Kata". It's presented here as an introduction to TDD.
Following TDD (Red, Green, Refactor) implement a class, BowlingGame, that calculates a single player's score in a bowling game. The class has two methods
// Call roll() any number of times in succession to record the number of pins knocked down by the player's balls
void roll(int pins);
// Call score() at any time to calculate the score
int score();
There's no requirement to score individual frames, or or determine when an complete game has been played.
A bowling game consists of 10 frames. In each frame the player has to try to knock down 10 pins, by bowling two balls at them. If the player knocks down all 10 with the first ball, the frame ends and the second ball isn’t bowled.
The score for the frame is the total number of pins knocked down, plus bonuses for strikes (all pins knocked down by the first ball)
and spares (all pins knocked down by the end of the frame). The bonus for a spare is the number of pins knocked down by the next roll.
So if in one frame, a player scored 5 and 5 (a spare) and in the next, they score 2 and 3, their score for the first frame is 5 + 5
(the number of pins knocked down in the frames) + 2 (the bonus for the spare). That is, 12. The next frame scores 2 + 3, i.e. 5.
The bonus for a strike is the number of pins knocked down by the next two rolls. So if in one frame, a player scored 10, and the in next frame
10 and the in one after 10 again, then the score for the first frame is 10 + 10 + 10, i.e. 30.
A player who rolls a spare or strike in the tenth frame is allowed to roll extra balls to allow the bonus for the frame to be scored.
- Clone the repository. This creates a new git repository on your local machine.
- Open the solution file in Eclipse. You will see the package siemens.plm.bowlinggame.tests. siemens.plm.bowlinggame is an empty package, where you will develop the BowlingGame class. siemens.plm.bowlinggame.tests is the project where you will develop the tests. siemens.plm.bowlinggame.tests already contains a file, BowlingGameTests.java, which contains the skeleton of your first test.
- Build the solution.
- Run the tests. They should all pass.
- Open the file BowlingGameTests.kava. The first test, when20GutterBallsRolled_ScoreShouldBe0, has been started for you.
- Implement the test as follows
- Creates a new BowlingGame object. Don't worry that the code won't complile, just carry on coding.
- Use it to call roll(0) 20 times
- Call the game's score() method to request the score.
- Finally the test checks that the score is 0.
- Now implement just enough code to get the assert to fail. You will have to
- Write a new BowlingGame class in the bowlinggame package.
- Create a new method in BowlingGame, void roll(int pins). Leave the method body empty.
- Create another new method in BowlingGame, int score(). Implement the method by returning -1. (Why don't we return 0 here?)
- Build the code, ensure it compiles and links.
- Run the tests. The test should fail, with an expected value of 0, and an actual value of -1.
Nice.
In the green stage, we write just enough code to get the test to pass. So
- Change the implementation of BowlingGame.score() to return 0.
- Compile and run the tests.
- Verify that the test passes.
- Commit your changes to your local git repository.
- Look over your test and see if there's anything - variable names, use of whitespace, etc. - that could be improved. Each time you make an improvement, run the tests again to verify they pass, and commit the changes to the git repository.
- Create a new test in the test package, called givenTwenty1sBowled_WhenGameScoreCalculated_ShouldBe20.
- Implement the test. Don't worry about any duplication between the test you're writing and the one that's already there; you'll deal with that in the refactor step.
- Run it and verify that it fails on the assert.
- Implement just enough code in roll() and score() to get both tests passing. The implementation should do no more than keep a running total of the number of pins scored; the rest of the implementation can wait until we have a test to validate it.
- Run the tests and verify that they pass.
- Commit the code to the git repository.
- Go back to the tests, and remove the duplication in the calls to roll() by factoring out a useful common method that both tests can use.
- Compile and run the tests to verify they still both work.
- Commit to git.
- Look over your tests and the BowlingGame class to check whether there's anything else that could be improved. Each time you make an improvement, run the tests again to verify they pass, and commit.
- Write a failing test to capture the bonus after a spare. The simplest test we can write is:
- the first frame is a spare (say, 0 then 10)
- the next ball scores (say, 3)
- the remaining balls all go down the gutter
So implement the test, givenOneSpareThen3Bowled_WhenGameScoreCalculated_ShouldBe16. 2. Compile and run the test. 3. Verify that it fails on the assert.
To implement spares, we're going to need some information about frames. We're going give the responsibility of monitoring the state of a frame to another class FrameState. We're going to calculate spare bonnus using a third class SpareBonus. We'll implement both these classes using TDD, starting out with writing a failing test. But we've got a problem: we've already got one test failing; now we need to write another failing test before we can get this one to pass. There are a few options
- Mark the current failing test to be ignored, so that it's not run when we run the tests.
- Revert the last commit and go back to the point where all the tests were passing.
- Stash the changes we've made in Step8 away somewhere.
Each has its merits. Here we're going to go with the last option.
- Create a stash of the changes made in Step8. You can do this in SourceTree by clicking on the Stash icon and giving the stash a suitable name.
FrameState has three methods
void roll(int pins)
boolean isSpare()
boolean isStrike()
isSpare() returns true whenever the last call to roll() marked the end of a frame that resulted in a spare.
All we need the Frame to do right now is to tell us whenever we're at the end of a Spare frame.
- Create a new test class, FrameStateTests in a new file, FrameStateTests.java in teh test package.
- Implement the test givenNoBallsBowled_FrameShouldNotBeASpare. The test should create a FrameState object, call isSpare() and verify that the result is false.
- Write just enough code to get the test to fail. When you create FrameState, implement it in the BowlingGame package. Take care when implementing isSpare(); you're aiming to get a failing test.
- Run the tests and verify that the test fails on the assert.
- Green. Get the test passing. Commit the code.
- Refactor. There's probably not a lot to do here, but check over your test to see if it can be improved. Commit after each improvement.
- Red. Write the next failing test: givenOneSpareFrameBowled_FrameShouldBeASpare. Run the test and get it to fail on the assert.
- Green. Implement the code to pass the test. Implement just enough to detect the end of the first frame. Commit.
- Refactor. Commit your improvements as you go.
- Red. Write the third test: givenOneSpareFrameFollowedByAZeroBall_FrameShouldNotBeASpare.
- Green. Implement sufficient code to pass the test. Commit.
- Refactor. Look at your code and your tests for any improvements. Commit as you go.
- Continue in the same way, adding the test, givenSeventeenOnesAndOneNine_FrameShouldBeASpare. Commit your changes as you go. At this point, FrameState successfully handles spares. It doesn't handle strikes, and it doesn't handle any bonus balls bowled in the 10th frame.
- Reinstate the test, givenOneSpareThen3Bowled_WhenGameScoreCalculated_ShouldBe16, from Step 8. You can do this SourceTree by expanding the "Stashes" node in the view on the left, right clicking on the stash you saved and selecting the option to apply it.
- Build the project.
- Run the tests. The new test should fail.
- Implement the behavior by introducing FrameState into BowlingGame. Run the test and verify it passes. Commit.
- Use TDD drive the implementation of FrameState.isStrike() for frames 1 - 10. Stick to the Red, Green, Refactor cycle and commit the code as you go. Start with the tests
- FrameStateTests.givenNoBallsBowled_FrameShouldNotBeAStrike
- FrameStateTests.givenOneStrikeFrameBowled_FrameShouldBeAStrike and add as many additional tests as necessary to complete the implementation. Try to choose your tests so that each one drives the implementation forward; you should be able to complete this step with five or six tests. Don't write anything to handle the bonus balls in the 10th frame yet.
- Use the test, BowlingGameTests.givenStrikeFollowedByThreeFourAndFive_WhenGameScoreCalculated_ShouldBe29 to implement scoring for strikes.
Consider carefully any improvements you can make to the code. Can you extract a class? e.g. to handle bonuses? If you find you want to create a new class, implement it using TDD.
The tenth frame is different, in that it can be
- a strike and two bonus balls
- a spare and one bonus ball
- or just two balls that are scored normally
- In Step 12, where we were implementing scoring for a spare, we could have gone on to write the test givenOneSpareThen18OnesBowled_WhenGameScoreCalculated_ShouldBe29 to validate that the bonus score only applies to the first ball following the spare. What would have been the advantages of doing this? Disadvantages?
- What tests did you include in Step 13? What was your reasoning?