Solution: Testing a Random Draw
Let’s start with the basics. We need a fresh bag before each test, and the first cases are the ones whose answers do not depend on the random draw.
public class BagTest {
private Bag bag;
@BeforeEach
public void setup() {
bag = new Bag();
}
@Test
public void newBagIsEmpty() {
assertEquals(0, bag.size());
}
@Test
public void addIncreasesSize() {
bag.add(BallColor.BLACK);
assertEquals(1, bag.size());
bag.add(BallColor.BLUE);
assertEquals(2, bag.size());
}
}
Testing the invariants of a random draw
We cannot say which ball remove returns, but a few things are true on every run, and each one becomes a test.
The first one is that remove on an empty bag throws.
@Test
public void removeFromEmptyBagThrows() {
try {
bag.remove();
fail("expected IllegalStateException when removing from an empty bag");
} catch (IllegalStateException e) {
return;
}
}
The second one is that a successful remove always drops the size by one.
@Test
public void removeShrinksSizeByOne() {
bag.add(BallColor.BLACK);
bag.add(BallColor.BLUE);
bag.remove();
assertEquals(1, bag.size());
}
The third one is that whatever remove returns must be something we put in. We cannot predict the color in general, but we can choose the input so that there is only one possible color. Fill the bag with black balls only, and the draw can only return black:
@Test
public void removeReturnsABallThatWasAdded() {
bag.add(BallColor.BLACK);
bag.add(BallColor.BLACK);
assertEquals(BallColor.BLACK, bag.remove()); // every ball is BLACK, so the draw must be BLACK
}
This looks like the flaky test, but it is not the same. By controlling the input we remove the randomness from the answer. The draw is still random, but the color it returns is fixed.
The last one is that remove returns a ball that was in the bag, and takes it out. Add several balls, say three black and two blue, then draw everything out and count how many balls of each color came out. We do not know the order, but the counts are fixed:
@Test
public void drainingReturnsEveryBallAdded() {
bag.add(BallColor.BLACK);
bag.add(BallColor.BLACK);
bag.add(BallColor.BLACK);
bag.add(BallColor.BLUE);
bag.add(BallColor.BLUE);
int black = 0, blue = 0;
while (bag.size() > 0) {
BallColor drawn = bag.remove();
if (drawn == BallColor.BLACK) {
black++;
} else {
blue++;
}
}
assertEquals(3, black);
assertEquals(2, blue);
assertEquals(0, bag.size());
}
Whatever order the five draws come in, the bag must return exactly three black and two blue and end empty.
These tests never assert an exact draw, so they pass against any correct Bag. The next section shows how to make the draw repeatable for one specific implementation.