In my
last post, we shook out a Theory about the number of bowls and frames in a game of bowling:
@Theory
public void shouldBeTenFramesWithTwoRollsInEach(Game game, Bowl first,
Bowl second) {
assumeNotNull(game, first, second);
assumeThat(game.isAtBeginning(), is(true));
assumeThat(game.getPlayers().size(), is(1));
assumeThat(first.isStrike(), is(false));
assumeThat(second.completesSpareAfter(first), is(false));
for (int frame = 0; frame < 10; frame++) {
game.bowl(first);
game.bowl(second);
}
assertThat(game.isGameOver(), is(true));
}
We used JUnitFactory to find some missing datapoints and missing assumptions, and got to the point where all JUnitFactory could find were parameters that either failed the assumptions or passed the tests, which is good.
At this point, we should think about the next functionality to test. I have almost a dozen methods that I've stubbed with fake answers, but before I completely forget about this Theory, I check the test that JUnit Factory has produced to test the "passing" path through this Theory:
public void testShouldBeTenFramesWithTwoRollsInEach() throws Throwable {
Game STARTING_GAME = BowlingTests.STARTING_GAME;
BowlingTests bowlingTests = new BowlingTests();
bowlingTests.shouldBeTenFramesWithTwoRollsInEach(STARTING_GAME, BowlingTests.THREE, new Bowl(100));
assertNotNull("bowlingTests.assume", getPrivateField(bowlingTests, "assume"));
}
Well, bust my buffers, where did JUnit Factory come up with the idea to bowl 100 pins with one ball? I'll have to look at its league records to see if there's been similar grade inflation. This is not the perfect test we'd like to see--no Bowl should exist with over 100 pins. I could fix this directly in the code, but we need a failing test first. Our current Theory would pass with 100 as a datapoint--what I need is a new Theory.
It's tempting to test that the Bowl constructor throws an exception whenever a pinCount over 10 is passed to it. However, testing for exceptions can be obfuscated, and it's not what I necessarily want to say--I want to say that no Bowl exists with more than 10 pins bowled:
@Theory
public void maximumPinCountIsTen(Bowl bowl) {
assumeNotNull(bowl);
assertThat(bowl.pinCount(), lessThanOrEqualTo(10));
}
This is easily passed by having
pinCount() always return, say, 5. I am being deliberately difficult here, employing what Kent Beck calls "Fake it till you make it". The correct implementation of
pinCount (return the pins passed in the constructor) is obvious, but it's worth our time to notice that our current tests don't distinguish between the obviously right and obviously wrong implementations.
The reason we can get away with a fake return from
pinCount is that all we've required of the method is that it return something less than or equal to 10. Let's add another Theory about the normal behavior of
pinCount:
@Theory
public void pinCountMatchesConstructorParameter(int pinCount) {
assertThat(new Bowl(pinCount).pinCount(), is(pinCount));
}
To make this pass, we can now put in the obvious definition of
pinCount:
private final int pinCount;
public Bowl(int pinCount) {
this.pinCount = pinCount;
}
public int pinCount() {
return pinCount;
}
Now all of our theories pass on our current data points. Let's look for other datapoints using JUnit Factory. We get this excellent test (from now on, I'll edit out all of the unimportant bits of the test, leaving the name and invocation
public void testMaximumPinCountIsTenThrowsAssertionError() throws Throwable {
bowlingTests.maximumPinCountIsTen(new Bowl(100));
}
This is what we were hoping for. Our theories now catch a 100-point Bowl as an error. Before going further, I need to add this as a data point:
public static ONE_HUNDRED_BOWL = new Bowl(100);
Now
maximumPinCountIsTen fails. To fix this, I'll prevent the construction of Bowls that have more than 10 pins:
public Bowl(int pinCount) {
if (pinCount > 10)
throw new IllegalArgumentException("At most 10 pins in one bowl");
this.pinCount = pinCount;
}
Now, everything falls apart. When trying to create an instance of
BowlingTests, the line
public static ONE_HUNDRED_BOWL = new Bowl(100);
causes construction to fail with an IllegalArgumentException, so no tests get run. We could remove the data point, but if we were ever to regress and forget to check arguments to the
Bowl constructor, this is a test that will remind us. Popper will allow us to wrap the datapoint in a method, annotated with
@DataPoint. Any
@DataPoint method that throws an exception is simply ignored, so we can keep this data point around in case it's ever needed again:
@DataPoint public Bowl oneHundredBowl() { return new Bowl(100); }
Running the tests now, we get an IllegalArgumentException on
pinCountMatchesConstructorParameter:
@Theory
public void pinCountMatchesConstructorParameter(int pinCount) {
assertThat(new Bowl(pinCount).pinCount(), is(pinCount));
}
Since any integer can be passed in, some of those integers will cause IllegalArgumentExceptions. However, these integers are invalid parameters. I could explicitly check that all ints coming in are between 0 and 10, but that would duplicate the logic that the constructor itself should be doing. Instead, I'll recognize that an IllegalArgumentException is a signal that the parameter is invalid:
@Theory
public void pinCountMatchesConstructorParameter(int pinCount) {
try {
assertThat(new Bowl(pinCount).pinCount(), is(pinCount));
} catch (IllegalArgumentException e) {
assumeNoException(e);
}
}
Here,
assumeNoException turns an otherwise fatal exception into an assumption failure.
Now, we run the tests again to find out that we haven't supplied any valid integers as parameters to the
Bowl constructor. We can easily come up with one ourselves, but let's overuse JUnit Factory instead. We generate tests, and JUnit Factory chooses the data point 0 to test
pinCountMatchesConstructorParameter. We add that data point to our TheoryContainer, and find that:
- Our tests pass.
- JUnit Factory can only find tests for normal returns and InvalidTheoryParameterExceptions, which is good.
- All the normal-return tests use sensible parameters.
Now, I'm finally ready to move on to my next bit of functionality, but I'll let this series on bowling with Popper and JUnit Factory draw to a close. For those of you following along at home, here's the
final source of our Theory class. Some things to note:
- We took incredibly small steps here. Most of the times that we invoked JUnit Factory, it found things we could have found ourselves. But since this is still a new habit for me, taking it slow gives me confidence I'm not missing anything
- Unlike in test-only-driven development, I expect that sometimes old theories will fail as I'm incrementally solving new ones and adding new data points. This is the theories doing their job. This initial theory was very precise about how the number of bowls in a frame corresponds to whether the first bowl is a strike, but says nothing about how to determine what a strike is. When I make the definition of "strike" more precise, I may also need to add more precision to my Bowl-counting algorithm to adjust to that. In test-only-driven development, this might require me to write a new test for the new data. In test-and-theory-driven development, the new test springs forth as a new data point or definition applied to an old theory.
- Popper and JUnit Factory are still evolving, and there's a lot of room for improvement in how they work together. If you have suggestions of how to make this process easier, please mail me.
- This is just one example of how we can "mash-up" data from a characterization service like JUnit Factory in order to promote quality. Other examples could include picking out lines of code that are difficult for JUnit Factory to cover, in order to suggest design improvements, or looking for patterns in exception-catching tests in order to suggest better patterns of error recovery. The sky's the limit.