Share and Enjoy

David Saff's blog of technological joys.

Tuesday, May 08, 2007

 

Exploring with JUnit Factory: 103 points in the first frame

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:



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:


Comments: Post a Comment





<< Home

Archives

February 2005   June 2005   March 2006   August 2006   December 2006   April 2007   May 2007   January 2008  

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]