Giter Site home page Giter Site logo

kata-game-of-craps's Introduction

Code Kata: Game of Craps

This repository contains code developed during the Pragmatic Unit Testing Code Katas.

The only rule (for now) on this project is to learn! Sorry, there's a second rule: we'll need to create meaningful commit messages.

Requirements

  • IntelliJ IDEA CE
  • Java 8

Building the project

On the command line, run:

$ ./gradlew build
$ ./gradlew buildComplete

The difference between the tasks is that the second will also generate a code coverage report and the API documentation.

Check the build/ directory after running one of these tasks.

Running the tests

On the command line, run:

$ ./gradlew test

Playing Craps!

After building the project, run:

$ java -cp build/classes/main gameofcraps.PlayCraps 1000

Acknowledgments

The Game of Craps exercise was recommended by @nelsonsar, which is the person to go for everything related to TDD.

kata-game-of-craps's People

Contributors

eriksencosta avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

kata-game-of-craps's Issues

Code review session 2

These are my code comments for the session 2 (2017-10-25).

Before, a quick recap on some important things.

Recap

Baby steps

We defined that we would test the class invariants: the conditions that should hold true during the entire class lifetime.

But we should test each condition separately. For example, the following exception does not help:

if (wins < 0 && losses < 0) {
    throw new IllegalArgumentException("Number of wins and losses cannot be negative");
}

As a client developer, I need to debug the code to know which value is wrong when I read this exception message. The right way is:

if (wins < 0) {
    throw new IllegalArgumentException("Number of wins cannot be negative");
}

if (losses < 0) {
    throw new IllegalArgumentException("Number of losses cannot be negative");
}

Now, when I make a mistake, I know rapidly which value is wrong. So, let's check each condition independently and throw an exception for each violation.

After testing all conditions, we can refactor what can be refactored. Premature optimization is the root of all evil also when we write code. Most of the time we read code so, let's not be lazy to write code.

In the tests, it important to test for the exception message. Teams Frances Allen and Karen Jones tested for the exception message using different approaches (the Rules engine and the old school try/catch block, respectively).

Last but not least, the code coverage report is your friend to check if each condition is really tested!

Code coverage

Read the README file. The Grade task buildComplete will run the project's test suite and generate the code coverage report. Check it!

$ ./gradlew buildComplete

Java's code style

Let's check if we can make the IDEA's Reformat Code tool to work.

Anyways, it is important to be familiar with Java's code style. It is simple.

  • We'll keep using soft tabs (i.e. spaces) for indentation. 4 spaces per level
  • We'll also use the 120 as the line limit

Commit and exception messages

Commit and exception messages should follow the same rules we apply for any other text. For example, the following exception message has an error:

totalRolls must be Greater than zero

Greater should be written greater as it is not a proper name. totalRolls is right because it refers to a property name.

And as commit messages should explains what and why something was added/changed/removed, an exception message should also be clear. It's all about communication. When an exception happens, it should inform the developer what happened. Good example of exception message:

The sum of wins and losses must be greater than zero

The newspaper metaphor

From Uncle Bob's (Robert C. Martin) Clean Code book (p11):

Think of a well-written newspaper article. You read it vertically. At the top you expect a headline that will tell you what the story is about and allows you to decide whether it is something you want to read. The first paragraph gives you a synopsis of the whole story, hiding all the details while giving you the broad-brush concepts. As you continue downward, the details increase until you have all the dates, names, quotes, claims, and other minutia.

We would like a source file to be like a newspaper article. The name should be simple but explanatory. The name, by itself, should be sufficient to tell us whether we are in the right module or not. The topmost parts of the source file should provide the high-level concepts and algorithms. Detail should increase as we move downward, until at the end we find the lowest level functions and details in the source file.

A newspaper is composed of many articles; most are very small. Some are a bit larger. Very few contain as much text as a page can hold. This makes the newspaper usable. If the newspaper were just one long story containing a disorganized agglomeration of facts, dates, and names, then we simply would not read it.

This metaphor can be used for the test classes too. Think on the GameStatisticsTest class: the tests for the exceptional cases should come before the other test cases. The exceptions happens before the object can be created.

Team Frances Allen

Commit message

You improved a lot, well done!

But attention for the following:

265b5ac

On the commit body:

Remove unneded methods in GameStatistics class and change tests attributes as constants.
  • Typo: unneded -> unneeded
  • Grammar: attributes as constants -> attributes to constants

7912d3f

You can rewrite the message using the git rebase command. Check #2 for more details.

Code

  • Fix the code style violations (IDEA has an option for that)

GameStatistics

Code style:

  • There are unnecessary line breaks in the class
  • There is an unused import statement: IllegalFormatException
  • There is an unneeded class comment
  • Illegal space in the if statements' parentheses
  • Add the public modifier to the class constructor
import java.util.IllegalFormatException;

/**
 * Created by nayvir on 10/19/17.
 */
public class GameStatistics {

    // ...
    public final int averageRolls;
    public final float probabilityWin;


    GameStatistics (int wins, int losses, int totalRolls, int longestPlay, int numberOfWinsFirst, int numberOfLossesFirst, int averageRolls) {
        if ( wins < 0 )
            throw new IllegalArgumentException("Negative wins are not allowed");

        if ( losses < 0 )
            throw new IllegalArgumentException("Negative losses are not allowed");

         if (wins + losses == 0) throw new IllegalArgumentException("The sum of wins and losses must be greater than zero");

        if ( totalRolls < 1 )
            throw new IllegalArgumentException("totalRolls must be Greater than zero");

        this.wins = wins;
        // ...
        this.probabilityWin = wins / numberOfGames;





    }



}

Should be:

public class GameStatistics {

    // ...
    public final int averageRolls;
    public final float probabilityWin;

    public GameStatistics(int wins, int losses, int totalRolls, int longestPlay, int numberOfWinsFirst,
                          int numberOfLossesFirst, int averageRolls) {
        if (wins < 0)
            throw new IllegalArgumentException("Negative wins are not allowed");

        if (losses < 0)
            throw new IllegalArgumentException("Negative losses are not allowed");

        if (wins + losses == 0)
            throw new IllegalArgumentException("The sum of wins and losses must be greater than zero");

        if (totalRolls < 1)
            throw new IllegalArgumentException("totalRolls must be Greater than zero");

        this.wins = wins;
        // ...
        this.probabilityWin = wins / numberOfGames;
    }
}

Exception message:

The exception message:

totalRolls must be Greater than zero

Should be:

totalRolls must be greater than zero

GameStatisticsTest

Code style:

  • There is an unused import statement: Rule

Initialization:

@Before
public  void setup() {
    gameStatistics = new GameStatistics(WINS, LOSSES, TOTAL_ROLLS, LONGEST_PLAY, NUMBERS_WINS_FIRST_ROLL, NUMBERS_LOSSES_FIRST_ROLL, AVERAGE_ROLL);
}

As we discussed in #2, there is no need for this set up method. GameStatistics is a Value Object. So:

// ...
private static final int AVERAGE_ROLL = 15;
GameStatistics gameStatistics = new GameStatistics(WINS, LOSSES, TOTAL_ROLLS, LONGEST_PLAY, NUMBERS_WINS_FIRST_ROLL, NUMBERS_LOSSES_FIRST_ROLL, AVERAGE_ROLL);

Code explicity:

@Test
public void createGameStatisticsResultsProbabilityOfWin() {
    assertEquals(gameStatistics.probabilityWin, (WINS / NUMBERS_GAMES), 0);
}

I prefer to be clear on the values I expect. So:

assertThat(gameStatistics.probabilityWin, closeTo(0.4285, TOLERANCE));

Where TOLERANCE is a very small number:

private static final double TOLERANCE = 1e-6;

This way, the reader of the code knows in a first sight the expected value.

Properties names:

  • Methods names: Check #2
  • NUMBERS_GAMES -> NUMBER_OF_GAMES
  • NUMBERS_WINS_FIRST_ROLL -> NUMBERS_WINS_COMING_ROLL (it's important to name the properties and behaviors using the domain's language, which in this case is coming roll, not first roll)

Team Karen Jones

Commit message

cfad4bd

Commit message:

Validate that GameStatistics cannot be instantiated with negative Wins

Add test creatingGameStatisticsThrowsExceptionWhenWinsIsNegativeOnInstantiation
that validates that an IllegalParameter exception is thrown when instantiating
GameStatistics with wins Negative.
  • The subject exceeds the 50 character limit
  • Explaining that a test case was added in a commit message makes sense only when the commit is about adding a new test case

A better example:

Check GameStatistics wins invariant

There is no way that a game can have negative wins.

Code

  • Fix the code style violations (IDEA has an option for that)

GameStatistics

Exception type:

import java.security.InvalidParameterException;

This is an exception intended to be used by the Java internals. The right exception to use is IllegalArgumentException, which is available for every Java class without needing an explicit import.

So, remove this import and replace the InvalidParameterException with IllegalArgumentException.

Properties names:

  • losesInComingOutRoll -> lossesInComingOutRoll
  • winnerProbability -> winningProbability
  • winnerProbabilityInComingOutRoll -> winningProbabilityInComingOutRoll
  • losesProbabilityInComingOutRoll -> losingProbabilityInComingOutRoll

GameStatisticsTest

Initialization:

int numLoses, numWins, longestPlayedGame, rollsMade, winsInComingOutRoll, losesInComingOutRoll;
GameStatistics stats;

private static final int NUM_LOSSES = 10;
private static final int NUM_WINS = 10;
private static final int LONGEST_PLAYED_GAME = 10;
private static final int ROLLS_MADE = 10;
private static final int WINS_IN_COMMING_OUT_ROLL = 10;
private static final int LOSSES_IN_COMMING_OUT_ROLL = 10;


numLoses = 10;
numWins = -10;
longestPlayedGame = 34;
rollsMade = 150;
winsInComingOutRoll = 4;
losesInComingOutRoll = 3;

@Before
public void createGameStatistics() {
    stats = new GameStatistics(numWins, numLoses, longestPlayedGame, rollsMade, winsInComingOutRoll, losesInComingOutRoll);
}

I think the refactoring to constants wasn't finished. Check the comment for the Team Frances Allen (there is no need for this @Before method).

Code explicity:

Check the comment for the Team Frances Allen.

For example:

@Test
public void creatingGameStatisticsResultsInWinnerProbability() {
    double probability = ((double)numWins / (numWins + numLoses));
    assertThat(stats.winnerProbability(), closeTo(probability, 0.00005));
}

I would refactor to:

@Test
public void creatingGameStatisticsResultsInWinnerProbability() {
    assertThat(stats.winnerProbability(), closeTo(0.5, TOLERANCE));
}

This way I don't need to check the values for numWins and numLoses and calculate the probability in my head. TOLERANCE is a very small number:

private static final double TOLERANCE = 1e-6;

Properties names:

Let's not abbreviate:

  • NUM_WINS -> NUMBER_OF_WINS
  • NUM_LOSSES -> NUMBER_OF_LOSSES

Team Radia Perlman

Commit message

Code

  • Fix the code style violations (IDEA has an option for that)

GameStatistics

Code style:

  • There is no need to use checked exception for the constructor

    throws IllegalArgumentException

Delete it!

Exceptions:

Check "Baby steps" on "Recap" above.

if (numWins == 0 && numLoses == 0) {
    throw new IllegalArgumentException("Number of wins and loses cannot be zero.");
}
if (numWins < 0 || numLoses < 0 ) {
    throw new IllegalArgumentException("Number of wins and loses cannot be negative.");
}

Properties names:

Let's not abbreviate!

  • numWins -> numberOfWins
  • numLoses -> numberOfLosses
  • numGames -> numberOfGames
  • lengthLongestGame -> lengthOfLongestGame
  • numTotalRolls -> numberOfTotalRolls
  • averageNumRoll -> averageNumberOfRolls
  • numWinsOnComingOutRoll -> numberOfWinsOnComingOutRoll
  • numLosesOnComingOutRoll -> numberOfLosesOnComingOutRoll

GameStatisticsTest

Code explicity:

The threshold value of the closeTo match can be extracted to a constant:

@Test
public void calculateWinningOnComingPutRollProbability() {
    assertThat(gameStatistics.winningOnComingPutRollProbability, closeTo(0.16,0.01));
}

I would refactor to:

assertThat(gameStatistics.winningOnComingPutRollProbability, closeTo(0.16, TOLERANCE));

Where TOLERANCE is a very small number:

private static final double TOLERANCE = 1e-6;

Initialization:

  • Extract the 5 and 2 literals to constants (check #2 for "raw numbers")
private GameStatistics gameStatistics = new GameStatistics(GAMES_WON, GAMES_LOST, 5,TOTAL_ROLLS, 5, 2);

Properties names:

Let's not use idioms from other languages. "Raises error" is a Python idiom. Java "throws" exceptions.

  • raisesErrorWhenNumberOfGamesIsZero -> throwsExceptionWhenNumberOfGamesIsZero
  • raisesErrorWhenNunberOfWinsLessThenZero -> throwsExceptionWhenNunberOfWinsLessThenZero
  • raisesErrorWhenNunberOfLosesLessThenZero -> throwsExceptionWhenNunberOfLosesLessThenZero

Code review session 5

We discussed mock objects and test doubles. I will review the code for sessions 4, 5 and 6 on the next week.

Code review session 8

These are my code comments for the session 8 (2017-12-06).

For the individual review of the teams, I included a checklist to help you to not forget of the needed changes.

Recap

Run the test suite before pushing the code

It is not the first time I find committed code that does not work. It is important to run the entire test suite before pushing the code to the repository.

This is a requirement to practice Continuous Integration (which is a practice, not a tool!).

Stub for non-sequences Dice throws

From the last week review, I defined as an assignment the creation of a new stub. This is the code suggested for this new stub:

package gameofcraps;

class DeterministicDice extends Dice {

    private int result;
    private boolean rolled;

    DeterministicDice(Integer result) {
        this.result = result;
    }

    @Override
    public int getSumOfFaces() {
        return result;
    }

    @Override
    public void roll() {
        if (rolled) {
            throw new PreviouslyRolledDiceException();
        }

        rolled = true;
    }

    private class PreviouslyRolledDiceException extends RuntimeException {
        PreviouslyRolledDiceException() {
            super("The DeterministicDice has already been rolled.");
        }
    }
}

Now, the tests that don't need a sequence get more clear on their intents:

@Test
public void winsIfComingOutRollIsSeven() {
    CrapsGame game = new CrapsGame(new DeterministicDice(7));

    game.play();

    assertTrue(game.getWin());
}

By abstracting this behavior, our code reads better. We expect to prove the scenario using a Dice that will roll only once and its result will be deterministic.

SequencedDice exception

In the test classes that use the JUnit's Parameterized runner, it is useful to know which SequencedDice failed.

Change the exception message to read like this:

private class ExhaustedDiceException extends RuntimeException {
    ExhaustedDiceException() {
        super("The SequencedDice has been exhausted. Sequence: " + sequencedResults.toString());
    }
}

Using meaningful names for methods

All the implementations of the boundary conditions tests defined a method to return a collection of Dice annotated with @Parameterized.Parameters. However, the method was named data().

It is important to use meaningful identifiers. Better names include:

  • listOfSequencedDices()
  • sequencedDices()

And naming suggestion for the test case method:

  • winsIfNthRollIsEqualsToComingOutRoll
  • losesIfNthRollIsEqualsToSeven

Also, these tests make the test case winsIfNthRollIsEqualsToComingOutRoll from CrapsGameTest deprecated.

reset() and getNumRolls()

These methods need to be implemented. As play() is a command method, our test cases already use a query method to check if the behavior. But we can only determine if the behavior is right by checking the number of rolls:

@Test
public void winsIfComingOutRollIsSeven() {
    CrapsGame game = new CrapsGame(new DeterministicDice(7));

    game.play();

    assertTrue(game.getWin());
    assertThat(game.getNumRolls(), equalTo(1));
}

This must also be implemented for the test cases that check for sequences.

Team Frances Allen

Commits

ee4b0df:

The commit message:

Refactor CrapsGame and CrapsGame tests
Improve the code of the play() method in CrapsGame and create the mew classes
CrapsGameLosingSequencesTests and CrapsGameWinningSequencesTest.
  • Break a line between the subject and the body
  • Typo. mew -> new

9265280:

  • Typo. sequencences -> sequences

Code

  • Change the SequencedDice exception
  • Run the test suite, it is broken for the losing sequences (see the fix below)
  • Fix coding style violations (see below)
  • Move SequencedDice to the test/gameofcraps directory
  • Rename file SequencedDIce.java -> SequencedDice.java
  • Rename class CrapsGameLosingSequencesTests -> CrapsGameLosingSequencesTest
  • Rename class CrapsGameWinningSequencesTests -> CrapsGameWinningSequencesTest
  • Rename the data() method of the classes CrapsGameLosingSequencesTest and CrapsGameWinningSequencesTest
  • Rename the losingSequencedDicesTest() and winningSequencedDicesTest() methods of the classes CrapsGameLosingSequencesTest and CrapsGameWinningSequencesTest (suggestions on the recap)
  • Create the DeterministicDice stub
  • Remove the winsIfNthRollIsEqualsToComingOutRoll() method from the CrapsGameTest class
  • Implement CrapsGame reset() and getNumRolls() methods

Losing sequences fix

The logic is broken. The fix is simple:

} while (value != comingOutRoll && value != 7);

Coding style violations

CrapsGameLosingSequencesTest

  • Wrong indentation of the imports statements
  • Unused import (assertTrue)
  • In the @Parameters method, break a line for each SequencedDice object, it makes the code more readable

CrapsGameWinningSequencesTest

  • In the @Parameters method, break a line for each SequencedDice object, it makes the code more readable

Team Karen Jones

Commits

f5712b4:

Add CrapsGamePlayTest class

 This class use a parameterized test to validate win and loss combinations as input of SequencedDice
  • The body line exceeds the 72 character length
  • There is an extra space in the beginning of the body line

Code

  • Change the SequencedDice exception
  • Fix coding style violations (see below)
  • Break the CrapsGamePlayTest into two classes to simplify the test setup (see the implementation from the other teams, they're simpler)
  • After breaking the CrapsGamePlayTest class, pay attention to the methods names (see the recap)
  • Create the DeterministicDice stub
  • Remove the winsIfNthRollIsEqualsToComingOutRoll() method from the CrapsGameTest class
  • Implement CrapsGame reset() and getNumRolls() methods
  • Check #3 for a better way to initialize The GameStatistics object on GameStatisticsTest
Coding style violations

CrapsGamePlayTest

  • Unused import (org.junit.Assert.assertTrue)

CrapsGameTest

  • Unused import (org.mockito.Mockito.*)

Team Radia Perlman

Commits

75b0f3e:

Refactor CrapGame tests
Refactor logic of method play
Add Parameterized runner for test sequences

Code

  • Change the SequencedDice exception
  • Rename the data() method of the classes CrapsGameLoseSequencesTest and CrapsGameWinSequencesTest
  • Rename the RunSequenceLoseTests() and RunSequenceWinTests() methods of the classes CrapsGameLoseSequencesTest and CrapsGameWinSequencesTest (suggestions on the recap)
  • Create the DeterministicDice stub
  • Implement CrapsGame reset() and getNumRolls() methods

Code review session 3

These are my code comments for the session 3 (2017-11-01).

Recap

Commit messages

Please, pay more attention to the commit messages. As we discussed, they're about communicating effectively the intentions of the changes. Please, review and discuss the message in group before pushing to GitHub.

The newspaper metaphor

Check #2 and refactor the GameStatisticsTest accordingly.

Team Frances Allen

Commit message

02d4cf6

Please, reword this commit message.

Team Karen Jones

Commit message

00de31e

On the commit's body:

Wins, losses, lossesInComingOutRoll, longestPlayedGame and lossesInComingOutRoll cant be negative

It should be:

Wins, losses, lossesInComingOutRoll, longestPlayedGame and lossesInComingOutRoll can't be negative.

07ead5c

Checks GameStatistics losses invariant

Typo: Checks -> Check.

A better word would be Validate:

Validate GameStatistics' losses invariant

65302db

Same as the previous suggestion.

f7496b9

This commit message is devoid of meaning. Also, the commit contains three different kinds of change:

  • Change the exception type
  • Rename variables (refactoring)
  • Use constants instead of variables (refactoring)

Ideally, you should not create a commit with different changes. A commit must contain related changes.

As it is already done, I would at least create a more meaningful message:

Refactor code and change exception type

This commit contains the following changes:

- Change the exception type of GameStatistics
- Rename variables and methods of GameStatistics
- Use constants instead of variables in GameStatisticsTest

61dfa01

Please, reword this commit message.

fd590ec

Similar to 07ead5c.

Code

GameStatistics

Exception messages:

Check all exception messages. The variable name in the commit messages must not be different than the one found in the program's text. Example:

throw new IllegalArgumentException("Wins cannot be negative");

Should be:

throw new IllegalArgumentException("wins cannot be negative");

The variable name is wins and not Wins.

Code style:

public int playedGames()
{

The opening brace must come in the same line as the method name.

public double winningProbability(){

A space must precede the opening brace. The same is true for the other methods.

GameStatisticsTest

Initialization:

Check #3 on this subject. The GameStatistics object does not need to be initialized in the @Before method.

ExpectedException:

There is no need to define it eight different times with eight different names. You can declare it once:

@Test
public void creatingGameStatisticsThrowsExceptionWhenWinsIsNegativeOnInstantiation() {

    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("Wins cannot be negative");

    new GameStatistics(-1, NUMBER_OF_LOSSES, LONGEST_PLAYED_GAME, ROLLS_MADE, WINS_IN_COMMING_OUT_ROLL,
            LOSSES_IN_COMMING_OUT_ROLL);
}

@Test
public void creatingGameStatisticsThrowsExceptionWhenLossesIsNegativeOnInstantiation() {

    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("Losses cannot be negative");

    new GameStatistics(NUMBER_OF_WINS, -1, LONGEST_PLAYED_GAME, ROLLS_MADE, WINS_IN_COMMING_OUT_ROLL,
                LOSSES_IN_COMMING_OUT_ROLL);

}

Team Radia Perlman

Commit message

94c9c76

You don't need the commit's body. Just the commit's subject is enough.

Code

GameStatistics

Constants:

You extracted the exception message to constants. Good idea!

Invariants:

There are missing and wrong invariants. wins and losses can be zero: I can be lucky or not, this is possible. Check the implementation of the other teams, they found other invariants.

GameStatisticsTest

Initialization:

Extract the 5 and 2 literals to constants (check #2 for "raw numbers").

private GameStatistics gameStatistics = new GameStatistics(GAMES_WON, GAMES_LOST, 5, TOTAL_ROLLS, 5, 2);

Dead code:

There is commented code in the file (lines 62 and 64). Remove them.

IsCloseTo

Why did you copied this class? You should had installed the hamcrest-all library to Gradle. The other teams did that.

Code review session 6

These are my code comments for the sessions 4, 5 and 6 (2017-11-08, 2017-11-15 and 2017-11-22).

Recap

Please, pay attention to the recap. Much of the issues here were recapped on the first three reviews. Attention to details matters.

Coding style

From #2:

Let's check if we can make the IDEA's Reformat Code tool to work.

Anyways, it is important to be familiar with Java's code style. It is simple.

  • We'll keep using soft tabs (i.e. spaces) for indentation. 4 spaces per level
  • We'll also use the 120 as the line limit

Commit messages

From #2:

Commit and exception messages should follow the same rules we apply to any other text. For example, the following exception message has an error:

totalRolls must be Greater than zero

Greater should be written greater as it is not a proper name. totalRolls is right because it refers to a property name.

And as commit messages should explains what and why something was added/changed/removed, an exception message should also be clear. It's all about communication. When an exception happens, it should inform the developer what happened. Good example of exception message:

The sum of wins and losses must be greater than zero

From #3:

Please, pay more attention to the commit messages. As we discussed, they're about communicating effectively the intentions of the changes. Please, review and discuss the message in group before pushing to GitHub.

The newspaper metaphor

From #2:

From Uncle Bob's (Robert C. Martin) Clean Code book (p11):

Think of a well-written newspaper article. You read it vertically. At the top you expect a headline that will tell you what the story is about and allows you to decide whether it is something you want to read. The first paragraph gives you a synopsis of the whole story, hiding all the details while giving you the broad-brush concepts. As you continue downward, the details increase until you have all the dates, names, quotes, claims, and other minutia.

We would like a source file to be like a newspaper article. The name should be simple but explanatory. The name, by itself, should be sufficient to tell us whether we are in the right module or not. The topmost parts of the source file should provide the high-level concepts and algorithms. Detail should increase as we move downward, until at the end we find the lowest level functions and details in the source file.

A newspaper is composed of many articles; most are very small. Some are a bit larger. Very few contain as much text as a page can hold. This makes the newspaper usable. If the newspaper were just one long story containing a disorganized agglomeration of facts, dates, and names, then we simply would not read it.

This metaphor can be used for the test classes too. Think on the GameStatisticsTest class: the tests for the exceptional cases should come before the other test cases. The exceptions happen before the object can be created.

In other words: the test cases for the exceptions must come first in the GameStatisticsTest class.

How to test CrapsGame

On the last discussion session, we agreed to implement a stub for Dice to drive the testing of the CrapsGame class.

Here is a sample stub for the class Dice (place it under the test/gameofcraps directory) with canned responses for a sequence of dice rolls:

package gameofcraps;

import java.util.List;
import java.util.Arrays;

class SequencedDice extends Dice {
    private final List<Integer> sequencedResults;
    private int rolls = -1;

    SequencedDice(Integer ...results) {
        sequencedResults = Arrays.asList(results);
    }

    @Override
    public int getSumOfFaces() {
        return sequencedResults.get(rolls);
    }

    @Override
    public void roll() {
        rolls++;

        if (rolls >= sequencedResults.size()) {
            throw new ExhaustedDiceException();
        }
    }

    private class ExhaustedDiceException extends RuntimeException {
        ExhaustedDiceException() {
            super("The SequencedDice has been exhausted");
        }
    }
}

Then, on our CrapsGameTest class:

@Test
public void winsIfComingOutRollIsSeven() {
    CrapsGame game = new CrapsGame(new SequencedDice(7));

    game.play();

    assertTrue(game.getWin());
}

@Test
public void winsIfComingOutRollIsEleven() {
    CrapsGame game = new CrapsGame(new SequencedDice(11));

    game.play();

    assertTrue(game.getWin());
}

@Test
public void winsIfNthRollIsEqualsToComingOutRoll() {
    CrapsGame game = new CrapsGame(new SequencedDice(4, 5, 4));

    game.play();

    assertTrue(game.getWin());
}

There are two opportunities for improvement in the suggestion above. Can you find it out after implementing all the test cases of the CrapsGameTest class?

The Dice class

The team Karen Jones changed the interface of the Dice class:

package gameofcraps;

public class Dice {
    // ...

    // before:
    // public void roll() {}
    public int roll() {
        face1 = getRandInt1To6();
        face2 = getRandInt1To6();

        return getSumOfFaces();
    }
}

This is not allowed in this exercise. We agreed to not change the interfaces of the classes.

Remember from chapter 9, on Command-Query Separation (location 3281):

A method that both returns a value and generates a side effect (changes the state of the class or some other entity in the system) violates the principle known as command-query separation. The principle states that a method should either execute a command (do something that creates a side effect) or answer a query (return some value), but not both.

Team Frances Allen

Code

I could not understand why you continued to implement the solution using flags if we discussed that implementation code should not contain testing concerns (like the flags forceWin and forceLose).

Revert the code up to commit 0eff79e and start afresh.

Team Karen Jones

Commit message

The commit messages below:

7a74a9f Add Dice and CrapsGame logic.
10f932e Add test to validate wins in commit out roll.

Should not have the period. The first line of a commit message is the "title" of the commit. Titles do not have a period.

And the second commit has a typo: commit out roll -> coming out roll.

Code

Revert the Dice to the original implementation (where the roll() returns void).

You can continue the implementation of the tests using the Mockito library if you want to compare later with the other solutions that use a stub.

Team Radia Perlman

Code

Just check the newspaper metaphor on the recap section and refactor the GameStatisticsTest accordingly.

Code review session 9

These are my code comments for the session 9 (2017-12-13).

Recap

We are almost done: there are only two classes to implement now, Dice and PlayCraps.

Dice

We'll start with Dice. The problem is that the only behavior of Dice is random. So, how to test it?

The answer is simple: execute the roll() method repeatedly and check if the result is within the accepted range.

Remembering that this class is implemented with command and query separation so, don't change the return type of roll().

PlayCraps

That's was always our goal, we have everything in place to implement this class now. Remembering that the play() method will need to play a number of numGames and to return a GameStatistics with all the stats of the played games.

The main() method just need to call this same play() method but it needs to output the result in the console.

Team Frances Allen

Commits

56b576a:

Just take care to not merge two changes in a single commit. Always create an individual commit for a refactoring. Also, take care with the verb tense.

Implementation and refactor in CrapsGame

Refactor in CrapsGameWinningSequencesTests and CrapsGameLosingSequencesTests,
test and implementation of reset and getNumRolls methods in CrapsGame.

I would prefer:

Implement CrapsGame numRolls and reset behaviors

Also, rename CrapsGame losing and winning tests files.
  • Improve commit message

Team Karen Jones

No comments.

Team Radia Perlman

Code

The newspaper methaphor

In CrapsGame, the private method rollDices appears before other public methods. Recap the newspaper metaphor. From Uncle Bob's (Robert C. Martin) Clean Code book (p11):

Think of a well-written newspaper article. You read it vertically. At the top you expect a headline that will tell you what the story is about and allows you to decide whether it is something you want to read. The first paragraph gives you a synopsis of the whole story, hiding all the details while giving you the broad-brush concepts. As you continue downward, the details increase until you have all the dates, names, quotes, claims, and other minutia.

We would like a source file to be like a newspaper article. The name should be simple but explanatory. The name, by itself, should be sufficient to tell us whether we are in the right module or not. The topmost parts of the source file should provide the high-level concepts and algorithms. Detail should increase as we move downward, until at the end we find the lowest level functions and details in the source file.

A newspaper is composed of many articles; most are very small. Some are a bit larger. Very few contain as much text as a page can hold. This makes the newspaper usable. If the newspaper were just one long story containing a disorganized agglomeration of facts, dates, and names, then we simply would not read it.

Missing test case in GameStatistics

I discovered you missed the implementation of a test case in GameStatistics. Run the code coverage tool (see the README file for instructions) and discover what is missing.

TODO

  • Move the private method rollDices to the end of the CrapsGame class
  • Find the missing test case of GameStatistics and implement it

Code review session 10

These are my code comments for the session 10 (2018-01-10).

Recap

PlayCraps tests

We finally implemented the PlayCraps class! However, it is necessary to test it with an acceptance test. The PlayCrapsTest already does that but it checks only if the returned GameStatistics is null or not.

It pays, in this specific case, to assert each metric to check if is not zero. For this, it is needed to play a lot of games. Better yet is to assert each metric with an explicit expected value. You are now capable of choosing the best strategy to create both kinds of test.

The main method can also be tested. The System Rules library provides tools to test the system output generated by a method.

As a side note, Team Radia Perlman used a language feature to format GameStatistics as a string.

Remember: the code coverage report is your friend.

  • Assert each metric in PlayCrapsTest, using two different kinds of test
  • Test the main method using the System Rules library

Commits

Please, pay attention to the commit & commit messages. Not just to the messages but also on the atomicity of the commit.

Last but not least, it is always good to run the test suite before pushing the changes to the repository. Care and discipline are important for a developer.

Team Frances Allen

Commits

ae253c6:

Put more effort before writing the commit message. We discussed this before. Also, this commit has two unrelated changes (the implementation of Dice and PlayCraps):

Implemnt Dice roll and PlayCraps main behaviors

Work in progress in PlayCraps main
  • Typo Implemnt -> Implement

Code

DiceTest

@Test
public void diceRandomValidTest() {
     boolean diceValidSum = true;
      for (int i = 0; i < 50; i++) {
           Dice dice = new Dice();
           dice.roll();
           if (5 <= dice.getSumOfFaces() && 12 >= dice.getSumOfFaces()){
           }else{
                diceValidSum = false;
           }
           assertTrue(diceValidSum);
     }
 }

Look, there is a lot of room for improvement here. The if/else isn't needed. You can use a different assertion or continue using the assertTrue but without the if/else statement.

Also, the sum of faces can be lower than five. If you had run the tests before committing it, you would see the error.

  • Simplify this code
  • Find a better name for the test method (there is a discussion about test case names in the book)
  • Fix CS issues

Team Karen Jones

Code

DiceTest

The code is good, the assertion is perfect! However, with two dices, we have a probability of getting a certain sum of faces that range from 1/36 (2) to 6/36 (7). So, with luck, we need at least 36 rolls to get each possible sum of faces. A hundred rolls will suffice.

  • Increase the number of rolls
  • Fix CS issues

Team Radia Perlman

Code

DiceTest

For a sound test, you need to test all possible results. See the Karen Jones comment.

  • Test all possible Dice results

Code review session 1

These are my code comments for the session 1 (2017-10-19).

Before, a quick recap on some important things.

Recap

Commit messages recap

From Tim Pope:

  • Subject line (summary), 50 characters max
  • Body (explanation), 72 characters max
  • Write the commit message in the imperative ("Fix bug" instead of "Fixed bug")
  • The first line should be a concise summary of the changes introduced by the commit; if there are any technical details that cannot be expressed in these strict size constraints, put them in the body instead.

The commit message from Radia Perlman is good. It explains the intentions of the GameStatistics class clearly.

Value Objects

Value Objects are (most of the time) immutable objects. Once created, it is impossible to change the state of a Value Object.

GameStatistics is a Value Object. Once created, there is no way to change the value of wins, for example.

Java's null return

From team Karen Jones, class PlayCraps:

public GameStatistics play(int numGames){
    return null;
}

I forgot the null value from Java. The goal was to create a simple acceptance test for PlayCraps. So an assertion is really needed in PlayCrapsTest. It can be as simple as:

assertFalse(gameStatistics == null);

Team Frances Allen

Commit message

Add Game Statistics

Game statistics test added and the implementation of the code using TDD. those  are the first tests to help the developer to make the game.
  • If you want to express you added a class, preserve the original name of the class in the summary (Add GameStatistics)
  • The explanation does not state the motivation for the existence of the class or its technical details. It does not matter that it was developed with TDD
  • The explanation violates the 72 characters limit

Code

  • Fix the code style violations (IDEA has an option for that)

GameStatistics

Initialization:

GameStatistics (int wins, int losses, int totalRolls, int longestPlay, int numberOfWinsFirst, int numberOfLossesFirst, int averageRolls){
    // ...
    this.probabilityWin = ProbabilityOfWin();
    // ...
}

public  float ProbabilityOfWin(){
    float  probabilityWin = wins / numberOfGames;
    return probabilityWin;
}

This initialization style is perfectly fine. But there is no need for probabilityOfWin to be public. And it certainly does not need a local variable:

private float probabilityOfWin() {
    return (float) wins / numberOfGames;
}

But as the calculations are so simple, they can be moved to the constructor.

GameStatisticsTest

Initialization:

public class GameStatisticsTest {
    int wins;
    int losses;
    // ...
    GameStatistics gameStatistics;

    @Before
    public  void setup(){
        wins = 3;
        losses = 4;
        // ...
        gameStatistics = new GameStatistics(wins, losses, totalRolls, longestPlay, numberOfWinsFirst, numberOfLossesFirst, averageRolls);

This initialization can be simplified. First, GameStatistics is a Value Object, so it's immutable. And objects can be initialized in the class body in Java. Second, the values used to create the GameStatistics can be extracted to constants:

public class GameStatisticsTest {
    private static final int GAMES_WON = 3;
    private static final int GAMES_LOST = 4;
    private static final int GAMES_PLAYED = 7;
    // ...
    GameStatistics gameStatistics = new GameStatistics(GAMES_WON, GAMES_LOST, // ...);

On constants, from Uncle Bob's (Robert C. Martin) Clean Code book (p300):

This is probably one of the oldest rules in software development. I remember reading it in the
late sixties in introductory COBOL, FORTRAN, and PL/1 manuals. In general it is a bad
idea to have raw numbers in your code. You should hide them behind well-named constants.
For example, the number 86,400 should be hidden behind the constant
SECONDS_PER_DAY. If you are printing 55 lines per page, then the constant 55 should be hid-
den behind the constant LINES_PER_PAGE.

Some constants are so easy to recognize that they don’t always need a named constant
to hide behind so long as they are used in conjunction with very self-explanatory code. For
example:

double milesWalked = feetWalked/5280.0;
int dailyPay = hourlyRate * 8;
double circumference = radius * Math.PI * 2;

Do we really need the constants FEET_PER_MILE, WORK_HOURS_PER_DAY, and TWO in the
above examples? Clearly, the last case is absurd. There are some formulae in which con-
stants are simply better written as raw numbers. You might quibble about the
WORK_HOURS_PER_DAY case because the laws or conventions might change. On the other
hand, that formula reads so nicely with the 8 in it that I would be reluctant to add 17 extra
characters to the readers’ burden. And in the FEET_PER_MILE case, the number 5280 is so
very well known and so unique a constant that readers would recognize it even if it stood
alone on a page with no context surrounding it.

Code explicity:

public void createGameStatisticsResultsTotalNumberOfGames() {
    assertEquals(gameStatistics.numberOfGames, (wins+losses));
}

The assertion is correct but I prefer to use a constant with a more explicit name:

assertEquals(gameStatistics.numberOfGames, GAMES_PLAYED);

A reader does not need to calculate on his mind the value of wins+losses.

Methods names:

Now that everything is tested and refactored, the methods names can be simplified. The Radia Perlman team used simple names yet effective names.

Team Karen Jones

Commit message

Create class GameStatistics

This class storage different statistics about Craps games.
  • The explanation could explain more about the
  • There is a typo in the explanation (games -> game)

Code

  • Typo in loses -> losses
  • Fix the code style violations (IDEA has an option for that)

PlayCraps

public GameStatistics play(int numGames){
    return null;
}

I missed the null return in Java. The goal was to create a simple acceptance test for PlayCraps, so, an assertion is really needed in PlayCrapsTest.

GameStatistics

Accessor vs calculated property:

public int playedGames() {
    return this.wins + this.loses;
}

This is just a matter of style (there is also a memory trade-off, marginal in this case). The only issue with Java is when you look from the client perspective:

int gamesWon = statistics.wins;
int playedGames = statistics.playedGames();

As the parentheses are mandatory for method invocation in Java, there is some unbalance to the eyes (negligible, a matter of taste).

Team Radia Perlman

Commit message

Add GameStatistics

GameStatistics class contains basic statistics for the Game of Craps
based on number of games played, number of wins and length of games.
It also has probabilities of winning and loosing.

This commit message is perfect.

The only mistake is the commit itself, which includes changes not related to GameStatistics.

Code

GameStatisticsTest

  • The names of the test methods are good. The initialization can be simplified, check comments on the Frances Allen team
  • The literals can be extracted to constants, check comments on the Frances Allen team

Code review session 7

These are my code comments for the session 7 (2017-11-29).

Recap

Refactoring the play method

On the play method of CrapsGame, we have a lot of conditional logic. The problem with these conditionals is that it makes harder to reason about what is going on.

All the implementations suffer with all these conditionals:

// Frances Allen
pairOfDice.roll();
numRolls++;
firstRoll = pairOfDice.getSumOfFaces();
if (firstRoll==7||firstRoll==11){
  win=true;
}
else if (firstRoll != 2 && firstRoll!= 3 && firstRoll !=12){
    do {
        pairOfDice.roll();
        numRolls++;
        value = pairOfDice.getSumOfFaces();

    }
    while (value != firstRoll || value == 7);
    if (value == firstRoll){
        win= true;
    }
}

// Karen Jones
pairOfDice.roll();
numRolls = 1;

int diceResult = pairOfDice.getSumOfFaces();
won = diceResult == 7 || diceResult == 11;
lose = diceResult == 2 || diceResult == 3 || diceResult == 12;

if (!won && !lose) {
    firstRoll = diceResult;

    do{
        pairOfDice.roll();
        numRolls ++;

        diceResult = pairOfDice.getSumOfFaces();
    }while(diceResult != 7 && diceResult != firstRoll);

    won = diceResult == firstRoll;
}

// Radia Perlman
int rollOut;
pairOfDice.roll();
int comingRollOut = pairOfDice.getSumOfFaces();

if (comingRollOut == 7 || comingRollOut == 11) {
    win = true;
} else if (comingRollOut == 2 || comingRollOut == 3 || comingRollOut == 12) {
    win = false;
} else {

    do {
        pairOfDice.roll();
        rollOut = pairOfDice.getSumOfFaces();

    } while (rollOut != comingRollOut && comingRollOut != 7);
    
    win = rollOut == comingRollOut ? true : false;
}

Can you feel the discomfort of having a do-while loop nested in an if-else statement? I feel!

I can refactor this code and replace the nested conditional with guard clauses (bookmark this catalog for future use!). A guard clause does one or all of the following:

  1. Checks the passed-in parameters, and returns with an error if they're not suitable
  2. Checks the state of the object, and returns if the function call is inappropriate
  3. Checks for trivial cases, and gets rid of them quickly

In our case, guard clauses can help on 3. The trivial part of this code is to check the winning and losing cases in the coming out roll. Using the Radia Perlman code as an example:

int rollOut;
pairOfDice.roll();
int comingRollOut = pairOfDice.getSumOfFaces();

if (comingRollOut == 7 || comingRollOut == 11) {
    win = true;
    return;
}

if (comingRollOut == 2 || comingRollOut == 3 || comingRollOut == 12) {
    return;
}

do {
    pairOfDice.roll();
    rollOut = pairOfDice.getSumOfFaces();
} while (rollOut != comingRollOut && comingRollOut != 7);
    
win = (rollOut == comingRollOut);

reset() and getNumRolls()

These methods need to be implemented. As play() is a command method, our test cases already use a query method to check if the behavior. But we can only determine if the behavior is right by checking the number of rolls:

@Test
public void winsIfComingOutRollIsSeven() {
    CrapsGame game = new CrapsGame(new SequencedDice(7));

    game.play();

    assertTrue(game.getWin());
    assertThat(game.getNumRolls(), equalTo(1));
}

Other tests cases

You all implemented the minimum test cases for the play behavior of the CrapsGame class:

  • Winning in the coming out roll
  • Losing in the coming out roll
  • A small winning sequence

But we need to exercise the boundary conditions. So, test for the following dice sequences:

  • Winning sequences
    • 4, 4
    • 4, 5, 4
    • 4, 5, 6, 4
    • 10, 10
    • 10, 9, 10
    • 10, 9, 5, 10
    • 10, 9, 5, 9, 10
    • 4, 2, 4 (snake eyes in the coming out roll is a special case)
    • 4, 3, 4 (3 in the coming out roll is a special case)
    • 4, 12, 4 (box cars in the coming out roll is a special case)
    • 4, 5, 6, 8, 9, 10, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 4
  • Losing sequences
    • 4, 7
    • 4, 5, 6, 7
    • 4, 5, 6, 8, 7
    • 4, 5, 6, 8, 9, 7
    • 4, 5, 6, 8, 9, 10, 7
    • 10, 7
    • 10, 9, 7
    • 10, 9, 5, 7
    • 10, 9, 5, 9, 7
    • 5, 2, 7 (snake eyes in the coming out roll is a special case)
    • 5, 3, 7 (3 in the coming out roll is a special case)
    • 5, 12, 7 (box cars in the coming out roll is a special case)
    • 4, 5, 6, 8, 9, 10, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 5, 6, 8, 9, 7

My suggestion for this is to use the Parameterized runner available in JUnit.

A new stub in the game

The SequencedDice stub made the code cleaner and extremely readable. However, for the cases that test only the coming out roll, we're not really testing a sequence.

Implement a new stub for the coming out roll tests and refactor them accordingly.

Team Frances Allen

Code

  • Fix the coding style violations on CrapsGame
  • Move SequencedDice to the test/gameofcraps directory

Team Karen Jones

Commits

See #7.

Additionally, the commit 8faa3dc have two problems:

  • The commit message is devoid of meaning
  • It includes changes on .gitignore, build.gradle and CrapsGame, Dice, GameStatistics and, GameStatisticsTest classes

I suggest using Git on the command line to get more the feel of versioning files and committing more of these mistakes.

As it takes much work to disassemble a commit into various commits, I recommend finding a better commit message.

Code

  • Fix the coding style violations on CrapsGame

Team Radia Perlman

Commits

You should merge the commits 2231bd8, cc25107 and f82fbc4 into a single commit. They form a single reason for change, together, they're an atomic change.

Code

You implemented the reset and getNumRolls but without implementing tests!

Code review session 4

I will review the session 4 and 5 together. There were not sufficient enough code in session 4 to review.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.