core-jgi/fitnesse/FitNesseRoot/FitNesse/UserGuide/FixtureCode/content.txt

196 lines
13 KiB
Plaintext

!*< hidden
!define TEST_SYSTEM {slim}
*!
[[A One-Minute Description][<FitNesse.UserGuide.OneMinuteDescription]]
[[A Two-Minute Example][<FitNesse.UserGuide.TwoMinuteExample]]
!1 Under The Covers : Fixture Code
Let's look at our division test table again:
|eg.Division|
|numerator|denominator|quotient?|
|10|2|5.0|
|12.6|3|4.2|
|100|4|33|
What '''really''' happens when we click that test button? (Try clicking it again.) What gets run?
* First, FitNesse submits the test table to [[Slim][<UserGuide.SliM]], the underlying test system (FitNesse is a wiki GUI built on top of Slim).
* Slim looks for and runs the fixture code corresponding to the test table.
* By design, the fixture code calls some underlying application code to do the real work, and report results back to the fixture
* Fit gives Fitnesse back the results from running the fixture code
* FitNesse figures out whether to turn table cells red or green (or yellow, if an exception was thrown) and displays the results page accordingly
!3 What is Fixture Code?
The fixture is the Java (or some other supported language) class that Slim to process the contents of the table. In the top row of the table above, "eg" specifies a Java package (or other language namespace), and "Division" specifies the actual class to be called.
Here is what the Java version of the Division class might look like:
{{{
public class Division {
private double numerator, denominator;
public void setNumerator(double numerator) {this.numerator = numerator;}
public void setDenominator(double denominator} {this.denominator = denominator;}
public double quotient() {return numerator/denominator;}
} }}}
!3 How Slim Uses Fixture Code to Process the Test Table
The Division table is an example of a [[Decision Table][<UserGuide.SliM.DecisionTable]]; just one of many different kinds of tables. Slim processes the rows of example data ('''from left to right'''), it stores the values from the ''numerator'' and ''denominator'' columns in the corresponding fields in our Division class using the setter functions. For each row, it then calls the quotient() method on our Division class, which returns what we would expect it to: the numerator divided by the denominator. So the Decision Table directs Slim to call setNumerator(10) and setDenominator(2); and then to call quotient(). The value is returned to the Decision Table which colors the appropriate cell.
Click the Test button again and see what happens to the column of expected outputs under the ''quotient?'' header. For each cell of expected output, the Decision Table compares the value it expects to get back with what it actually gets. If the return value matches the value in the corresponding table cell, FitNesse turns the cell green. Otherwise FitNesse turns the cell red, and shows both the expected and actual values. (If Slim encounters an exception, or cannot find a fixture, field, or method, it turns the corresponding table cell yellow, and inserts a stack trace.)
!3 Wait a Minute. That's ''Too'' Simple.
How right you are. In the real world, the fixture code would not be doing any of the real work (though a single division operation is not much work). The fixture code would delegate to real application code, which in turn would do the work. In general, fixture code should be '''as thin as possible'''. It should be nothing but '''piping and wiring''' between the FitNesse table and the application code under test.
So with that bit of wisdom in our minds, let's look at something a bit more realistic.
!2 Something a Bit More Realistic: A Trivia Game
As much as we might like to, we shall not immediately leap into an application to analyze the seismic data resulting from setting off dozens of sticks of dynamite on the floor of the Gulf of Mexico. Partly because so many fish would die, and partly because that's going too far in the complexity direction.
Instead let's imagine that we are building a trivia game. The overall design of our trivia game is straightforward: players take turns rolling a single die, and move around a circular board. When they land on a square, they are asked a trivia question of some category. There will be requirements and test tables later on for answering questions incorrectly, for winning, and so on.
For now let's imagine that we are addressing a specific first requirement or user story (call it what you like):
!3 "You can add players to the game, and you can ask the game how many players are playing."
Sounds pretty straightforward. Let's first set up a ClassPath that points to where our trivia game project is. Without the ClassPath, FitNesse would not be able to find our fixture code:
!path C:\workspace\TriviaGameFitNesseExample\
How about this for a test table?
!| org.fitnesse.triviaGameExample.fitnesseFixtures.AddRemovePlayerFixture|
| playerName | addPlayer? | countPlayers? |
| Al | true | 1 |
| Bertha | true | 2 |
It says that if we add a player named Al to our game successfully, the total number of players should be 1, and if we then add a player named Bertha, our total number of players should be 2.
!3 The Code for !-AddRemovePlayerFixture-!
What might the fixture code for that look like (so far)? How about this: {{{
import fit.ColumnFixture;
public class AddRemovePlayerFixture {
private String playerName;
private Game theGame;
public void setPlayerName(String playerName) {
this.playerName = playerName;
}
public boolean addPlayer() {
theGame = StaticGame.theGame;
Player thePlayer = theGame.addPlayer(playerName);
return theGame.playerIsPlaying(thePlayer);
}
public int countPlayers() {
return theGame.getNumberOfPlayers();
}
} }}} Yes, I think we have arrived at a more realistic level of complexity. What does this code mean? Well, we have a setter named ''setPlayerName'', as required, and we have methods named ''addPlayer()'' and ''countPlayers()''. Straightforward enough.
But '''what is that private Game field, and what are those methods calling, and why'''?
!2 Piping and Wiring: Delegating to Real Code
Our fixture's ''addPlayer()'' method is indeed thin: it merely calls an ''addPlayer()'' method on a Game class, which does the real work. Here is that Game class, such as it currently is: {{{
public class Game {
private ArrayList players;
public Game() {
players = new ArrayList();
}
public Player addPlayer(String aPlayerName) {
Player aPlayer = new Player(aPlayerName);
players.add(aPlayer);
return aPlayer;
}
public boolean playerIsPlaying(Player aPlayer) {
return players.contains(aPlayer);
}
public int getNumberOfPlayers() {
return players.size();
}
} }}}
Game adds our players to an !-ArrayList-!, and returns the new Player object. The ''playerIsPlaying()'' method reports whether a player is playing, and ''getNumberOfPlayers('') returns the number of players in the collection. Not much of a trivia game yet, but it meets our one requirement: we can add players and count them.
Notice that our fixture code's ''addPlayer()'' method above calls ''playerIsPlaying()'' to determine whether a player was successfully added: a fairly meaningful return value. Notice that our fixture's ''countPlayers()'' is even thinner: it returns the result from a single call to ''getNumberOfPlayers()'' on Game.
But what is that call to !-StaticGame-! for?
!3 What That Call To !-StaticGame-! Is For
Each row of our table above involves a separate call to our ''!-AddRemovePlayerFixture-!'' class. Since we are adding players to the same game, we need to ensure that we are talking to the same Game object each time.
Furthermore, we will have several test pages for our suite of tests for the trivia game. Each of those test pages will use more than one table to set up and test a condition in a Game instance. We need it to be the same Game instance being tested by all the test tables on a page. So we need a Singleton Game instance for the tables and their corresponding fixture classes to share. Here is the code for !-StaticGame-!: {{{
public abstract class StaticGame {
public static Game theGame = new Game();
} }}}
It just a static variable that holds an instance of Game. And for safety's sake, it is abstract: you cannot instantiate !-StaticGame-!.
!2 Enabling Multiple Test Tables to Share a Common Object
OK. Let's justify that !-StaticGame-! thing a bit more thoroughly. Say we have another requirement that goes like this:
!3 "Once the game has started, players cannot be added or removed."
For this test, we'll ask the game to take a fake turn by specifying that the player whose turn it is "rolls" a 6. That should start the game. We'll check the result of that by checking to see which player it was who actually took the turn (we expect it to be Al), and whether indeed the game has started. Note that this fixture, in order to work properly, will need to talk to the same Game object that our !-AddRemovePlayerFixture-! talked to above. That's what !-StaticGame-! does for us.
!|org.fitnesse.triviaGameExample.fitnesseFixtures.GameTurnFixture|
|roll | player? | gameHasStarted? |
|6 | Al | true |
Now that the game has started, we'll try to add a new player to the game, and this should fail (we should get back false from addPlayer()). And we should still have only two players in the game:
!| org.fitnesse.triviaGameExample.fitnesseFixtures.AddRemovePlayerFixture|
| playerName | addPlayer? | countPlayers? |
| Joe | false | 2 |
Finally, we'll try to remove a player from the game, and this too should fail:
!| org.fitnesse.triviaGameExample.fitnesseFixtures.AddRemovePlayerFixture|
| playerName | removePlayer? | countPlayers? |
| Al | false | 2 |
This shows how you can use a sequence of tables to verify a requirement by setting up and testing different states in your application code.
Click the Test button to see how it all turns out.
!3 New Code for removePlayer()
Our removePlayer() on !-AddRemovePlayerFixture-! works much as addPlayer() does: {{{
public boolean removePlayer() {
theGame = StaticGame.getInstance();
thePlayer = theGame.getPlayerNamed(playerName);
theGame.removePlayer(thePlayer);
return (playerWasRemoved(thePlayer));
}
private boolean playerWasRemoved(Player aPlayer) {
return (!theGame.playerIsPlaying(aPlayer));
} }}}
It too shares the Game instance supplied by !-StaticGame-!. And you can see that our !-GameTurnFixture-! methods do the same: {{{
public class GameTurnFixture {
private int roll;
private Game theGame;
public String player() {
theGame = StaticGame.theGame;
return theGame.takeTurn(roll);
}
public void execute() {
StaticGame.theGame.takeTurn(roll);
}
public boolean gameHasStarted() {
theGame = StaticGame.theGame;
return theGame.gameHasStarted();
}
public void setRoll(int roll) {
this.roll = roll;
}
} }}}
So, as !-AddRemovePlayerFixture-! and !-GameTurnFixture-! are repeatedly called, they make changes to, and check the state of, a single Game object.
This is a common pattern. One way or another, Fitnesse test tables on a single page often need to share a common object. We grant you that there is more than one way to skin this particular cat. This is the way we chose to skin it. It will show up in examples ahead of us.
Note the ''execute'' method of !style_code(!-GameTurnFixture-!). This will be called after all the setters have been called, and just before the output functions are called.
!2 Summary
* A fixture is the class that FitNesse and Slim use to process a particular test table when the Test button is clicked. For each row of data in a test table, Slim sets its inputs using stter methods, and then calls the specified output methods. FitNesse uses the return values to determine whether to turn output table cells green or red.
* You need to use a ClassPath to specify to Slim where your fixture code resides.
* Fixture code should be as thin as possible: its methods should merely delegate to, and return values from, methods on application code. To process our player-adding test tables above when we click the Test button, Slim uses our !-AddRemovePlayerFixture-! Java class to pass data between the table and underlying Java application classes (Game and Player).
* Sometimes fixtures get involved in pulling together test data for input, and formatting returned data for display, but fixtures should contain no business logic.
* Often the test tables on a page need to share an object. We've illustrated the use of a static variable to solve this problem. Your mileage may vary.
!2 Learning More
To learn more about the different styles of test tables and the fixture code used to process them, see [[Slim][<UserGuide.SliM]].