Share and Enjoy

David Saff's blog of technological joys.

Friday, April 27, 2007

 

Bowling with Popper style

A few months ago, I released the first version of Popper, an extension to JUnit that allows you to supplement Tests (statements about how one particular object acts on one set of inputs) with Theories (statements about how all objects meeting certain criteria act on all inputs meeting other criteria). Popper has grown up to version 0.4, and I'd like you to try it out. Yes, you.

The big idea here is being even more precise about what you test, and how you communicate it. Tests written in a JUnit style end up saying both more and less than the developer knows--Theories help you say exactly what you know.

As an example, let's work through the a unit test Kevin Lawrence has suggested for a bowling scorer. The first requirement Kevin considers is


// 2.1.1 A game of tenpins consists of ten frames. A player delivers two balls in each of the first
// nine frames unless a strike is scored. In the tenth frame, a player delivers three balls if a
// strike or spare is scored. Every frame must be completed by each player bowling in regular
// order.


Here's Kevin's test:


@Test
public void shouldBeTenFramesWithTwoRollsInEach() {
for(int frame = 0; frame < 10; frame++){
game.bowl(3);
game.bowl(4);
}

assertThat(game.isGameOver(), is(true));
}


Kevin's using Hamcrest, a matcher library that allows the convenient assertThat syntax. This is good, because we will too.

This test talks about a fixture field game, which is initialized as such:


public class GameTest {
private Game game;

@Before
public void createGame() {
game = new Game();
}
}


This is a fairly well-written JUnit test setup, and it will correctly catch a number of bugs. However, as a communication tool to a future maintainer, it falls short:


  1. The requirement talks about each player bowling in regular order--does the default constructor of Game produce a single-player game?
  2. Does the property of having twenty bowls left only apply right after construction, or to Games in other states?
  3. What's special about 3 and 4? Would any other numbers do? Just those particular numbers?


When writing a theory, the strategy is to remove specifics that are not important to the behavior under consideration, and use the object's own protocol to fill in the details that are left out. First, we can remove the invocation of the default constructor from the understanding of the test--what's important is not the constructor call, but the state the game is in at the beginning of the sequence, and the number of players. We do this by making game a parameter of the method, which is now a Theory, and making assumptions about its current state:


@Theory
public void shouldBeTenFramesWithTwoRollsInEach(Game game) {
assumeThat(game.isAtBeginning(), is(true));
assumeThat(game.getPlayers().size(), is(1));

for(int frame = 0; frame < 10; frame++){
game.bowl(3);
game.bowl(4);
}

assertThat(game.isGameOver(), is(true));
}


In the future, it may be possible to have Games that are are "at the beginning", but don't result directly from constructor calls--for example, a Game loaded from an intermediate "save-game" file. This Theory will automatically apply to those Games, as well.

Next, what's special about 3 and 4? Well, they're just two numbers that indicate that neither a strike nor spare was bowled:


@Theory
public void shouldBeTenFramesWithTwoRollsInEach(Game game, int firstBowl, int secondBowl) {
assumeThat(game.isAtBeginning(), is(true));
assumeThat(game.getPlayers().size(), is(1));
assumeThat(firstBowl, lessThan(10));
assumeThat(firstBowl + secondBowl, lessThan(10));

for(int frame = 0; frame < 10; frame++){
game.bowl(firstBowl);
game.bowl(secondBowl);
}

assertThat(game.isGameOver(), is(true));
}


(Actually, we're missing the fact here that spares in the first nine frames also lead to two bowls--the original test left that fact out, and I choose to do the same in this Theory).

The line assumeThat(firstBowl + secondBowl, lessThan(10)) bothers me. It doesn't match up with the concept of "spare" from the requirements, and it likely duplicates logic that will end up in the domain soon enough. Therefore:


@Theory
public void shouldBeTenFramesWithTwoRollsInEach(Game game, Bowl first, Bowl 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));
}


Now, we've removed unhelpful particulars, and added some helpful domain concepts and explicit assumptions. How does this Theory get run? There's two answers. We can use this theory for validation (Does every parameter combination we've considered in the past still work?), or exploration (Is there any new parameter combination that passes the assumptions, but fails the assertions?).

For validation, the Theory method must be declared on a subclass of TheoryContainer, which causes it to be run with a custom JUnit runner. By default, the subclass is also expected to declare as constants any valid parameter values that are currently believed to pass the theory:


@RunWith(Theories.class)
public class BowlingTheories extends TheoryContainer {
public static Game STARTING_GAME = new Game();
public static Bowl GUTTER_BALL = new Bowl(0);
public static Bowl STRIKE_BALL = new Bowl(10);
public static Bowl THREE = new Bowl(3);
public static Bowl FOUR = new Bowl(4);

@Theory
public void shouldBeTenFramesWithTwoRollsInEach(Game game, Bowl first, Bowl second) {
// as above ...
}
}


The custom runner will try every possible combination of parameters from the set given, but it will not try anything outside that set.

Once I'm satisfied that my code passes the Theory for all the parameters I can think of myself, I'm ready for some automated exploration, to search for parameters that I haven't thought of. However, if your code is free from legal entanglements, JUnit Factory, from Agitar, works very well for exploration. More on that in the next post. Right now, here's some fun things to try:


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]