AcceptanceTestsWithFitNesse
Suite Results
Test Pages: 3 right, 1 wrong, 0 ignored, 0 exceptions     Assertions: 163 right, 2 wrong, 0 ignored, 0 exceptions
12 right, 0 wrong, 0 ignored, 0 exceptions GuiTest
8 right, 0 wrong, 0 ignored, 0 exceptions GuiTestWithOtherFixtures
33 right, 2 wrong, 0 ignored, 0 exceptions TestingDatasets
110 right, 0 wrong, 0 ignored, 0 exceptions TestingWithFitNesse

This is only the HTML version of the original WiKi-based tutorial.

You should download it from here to experience the real thing.

Test Output

< New Requirements | Gui Test with other Fixtures >

 Settings

UI-based TimeRecording Test

I can hear the FIT envangelists yelling Don't you never ever perform FIT tests based on the UI. Test the business logic for christ's sake. Well, nail me to the cross if you like. Testing the business logic is one thing, but this doesn't help if the graphical user interface is not working correctly. So if you are a first class hardliner, skip to Testing Datasets. You're still reading? So let's go on. FIT provides a special Fixture for testing UI interactions: The ActionFixture.

The ActionFixture is based on the following metaphor:
  • With enter you enter values into fields.
  • The action press means clicking a button.
  • With check you check values of fields.

Meaning with this Fixture you can simulate the user interactions. In order to fool around with this Fixture we created a tiny little GUI for our time recording system.



Now we would like to test our system using the ActionFixture by clicking the Test button:

de.fitsample.ActionFixture
slowMotion 200
start de.fitsample.timerecording.ui.TimeRecordingUIFixture
enter day 1
enter from 6:30
enter to 12:00
press add
enter from 13:30
enter to 16:15
press add
check working time 8.25
check sum 8.25
enter day 2
enter from 6:30
enter to 16:15
press add
check working time 9
check sum 17.25
enter day 3
enter from 10:00
enter to 12:00
press add
check working time 2
check sum 19.25
enter from 12:45
enter to 17:00
press add
check working time 6.25
check sum 23.5
enter day 4
enter from 8:00
enter to 12:00
press add
check working time 4
check sum 27.5
enter from 13:00
enter to 18:30
press add
check working time 9.5
check sum 33

Tried it? Cooool..., but how does this work? Let's have a look at the relevant UI code first. You will find this code in the classes

For us the focus is on the class TimeRecordingUI. Here an excerpt with UI components of interest:
public class TimeRecordingUI extends JFrame {

protected JButton addButton;
protected JTextField dayTextField;
protected JTextField fromTextField;
protected JTextField toTextField;
protected JTable timeRecordTable;
...

So the table describes the workflow and the expected results. But the real interaction with the UI components happens in our Fixture. One thing that's remarkable is that we are dealing with two Fixtures: The ActionFixture, and the Fixture that actually performs the interaction with the UI components. How does this work? Let's have a closer look to the cycle of an ActionFixture test:
  • In the first row the ActionFixture is created which then takes control.
  • The action start creates our real Fixture.
  • Now if the ActionFixture finds an enter, it will search for a method with a name that comes after enter and calls it with the value in the next cell as a parameter.
  • On press a method with the name found in the next cell is called.
  • On check a method with the name found in the next cell is called. The value returned be the method is compared to the expected value specified in the next cell.

That means in a table row like
|enter|from|9:00|
a method from() is called with the value 9:00 as parameter.
|press|add|
calls the method add()
|check|working time|9.5|
calls the method workingTime() and checks if the returned value is 9.5. If so, the cell is turned green, otherwise red.

This means our Fixture has to provide the appropriate methods. Let's see:
public class TimeRecordingUIFixture extends Fixture {

private TimeRecordingUI timeRecordingUI;

public TimeRecordingUIFixture() {
timeRecordingUI = new TimeRecordingUI();
timeRecordingUI.pack();
timeRecordingUI.setVisible(true);
}

public void day(String day) {
timeRecordingUI.dayTextField.setText(day);
}

public void from(String from) {
timeRecordingUI.fromTextField.setText(from);
}

public void to(String to) {
timeRecordingUI.toTextField.setText(to);
}

public void add() {
timeRecordingUI.addButton.doClick();
}

public double workingTime() {
TableModel model = timeRecordingUI.timeRecordTable.getModel();
Object value = model.getValueAt(model.getRowCount() - 1,
TimeRecordingTableModel.WORKING_TIME_COLUMN);
return ((Double) value).doubleValue();
}

public double sum() {
TableModel model = timeRecordingUI.timeRecordTable.getModel();
Object value = model.getValueAt(model.getRowCount() - 1,
TimeRecordingTableModel.SUM_COLUMN);
return ((Double) value).doubleValue();
}
}

The methods from(), to() and add() do not need any further explanation. workingTime() and sum() are not that hard either, they just retrieve the needed value from the TableModel.


Slow Motion

In the second row there is something we did not explain yet:
|slowMotion|200|

Well, if we would run the original fit.ActionFixture you could hardly follow the UI interactions, since they are much too fast. That's why we made our own extension de.fitsample.ActionFixture, which provides the additional (optional) command slowMotion. If slow motion is used the Fixture will take a nap after each command. The value after slowMotion is the duration of the nap in milliseconds, which is in our case a nap of 200ms.
public class ActionFixture extends fit.ActionFixture {

private long slowMotionMs;

public void slowMotion() {
slowMotionMs = Long.parseLong(cells.more.text());
}

public void enter() throws Exception {
super.enter();
sleep();
}

public void check() throws Exception {
super.check();
sleep();
}

public void press() throws Exception {
super.press();
sleep();
}

public void start() throws Exception {
super.start();
sleep();
}

protected void sleep() {
if (slowMotionMs < 1) {
return;
}
try {
Thread.sleep(slowMotionMs);
} catch (InterruptedException excep) {

excep.printStackTrace();
}
}
}


< New Requirements | Gui Test with other Fixtures >
< GUI Test | Testing Datasets >

 Settings

Alternatives to ActionFixture

So with ActionFixture we directly test the UI by simulating user interactions. This workflow oriented do-this-then-do-that approach is quite nice, but the resulting table is anything but intuitive. Our first attempt with the ColumnFixture was much more descriptive.

Is this the only way to perform UI tests with FIT? Nope. Can do it with any Fixture. For example with our well known ColumnFixture. Let's have a look at our UI again:



Now we write a table that represents the data on the UI as simple as possible:

de.fitsample.timerecording.ui.TimeRecordingUIColumnFixture
day from to working time? sum?
1 6:30 12:00   5.5   5.5
  1 13:30 16:15 8.25 8.25
2 06:30 16:15 9 17.25
3 10:00 12:00   2.0   19.25
  3 12:45 17:00 6.25 23.5
4 8:00 12:00   4.0   27.5
  4 13:00 18:30 9.5 33.0

And here we go, click the Test button...

...cool, the same user interactions we saw with our ActionFixture, but the table is much more compact. So how the heck does this work now? This is done in our new Fixture:
public class TimeRecordingUIColumnFixture extends ColumnFixture {

public String day;
public String from;
public String to;

private TimeRecordingUI timeRecordingUI;

public TimeRecordingUIColumnFixture() {
timeRecordingUI = new TimeRecordingUI();
timeRecordingUI.pack();
timeRecordingUI.setVisible(true);
timeRecordingUI.toFront();
}

public void execute() {
sleep();
timeRecordingUI.dayTextField.setText(day);
sleep();
timeRecordingUI.fromTextField.setText(from);
sleep();
timeRecordingUI.toTextField.setText(to);
sleep();
timeRecordingUI.addButton.doClick();
sleep();
}

public double workingTime() {
TableModel model = timeRecordingUI.timeRecordTable.getModel();
Object value = model.getValueAt(model.getRowCount() - 1,
TimeRecordingTableModel.WORKING_TIME_COLUMN);
return ((Double) value).doubleValue();
}

public double sum() {
TableModel model = timeRecordingUI.timeRecordTable.getModel();
Object value = model.getValueAt(model.getRowCount() - 1,
TimeRecordingTableModel.SUM_COLUMN);
return ((Double) value).doubleValue();
}

protected void sleep() {
int slowMotionMs = 200;
try {
Thread.sleep(slowMotionMs);
} catch (InterruptedException excep) {
excep.printStackTrace();
}
}
}


Hmm, not that complicated. The ColumnFixture assigns values to the members day, from and to, and execute() performs the UI interactions. workingTime() and sum() are pretty much the same as in our ActionFixture.

Alright, but what about that sleep() method? This is that slow motion thing again, which we already had in our ActionFixture.

< GUI Test | Testing Datasets >
< Gui Test with other Fixtures | Links and Books >

 Settings

The RowFixture

The ColumnFixture is good at testing functions; GUIs may be tested with ActionFixture. But sometimes the result of an operation is not a single piece of data but a dataset that must be tested against the expectations.

That's where the RowFixture comes into play. In a row RowFixture table one table row specifies an expected dataset. The RowFixture has the following properties:
  • The order of the datasets is not taken into account. The RowFixture identifies a dataset by equality of the row cells, starting from left to right.
  • Missing and surplus datasets will be rendered as failures.
Let's give it a try. We will use our time recording example again. Ok, it's not a database application; but at last our TimeRecording is nothing but a list of datasets. But at first we have to fill our TimeRecording with some values. How do we achieve that? Well, we just abuse our ColumnFixture for that and take care that the RowFixture starts working with the same TimeRecording instance.

Let's check it out by pressing Test:

At first we enter the datasets into the system using our TimeRecordingColumnFixture.
de.fitsample.timerecording.TimeRecordingColumnFixture
day from to
1 10:00 12:00
  1 12:45 17:00
2 08:00 12:00
  2 13:00 18:30
3 08:00 12:00
  3 13:00 19:30

After that, we test the expected datasets with a RowFixture. As you can see the order of the datasets is of no intereset:
de.fitsample.timerecording.TimeRecordingRowFixture
day? from? to?
3 08:00 12:00
1 10:00 12:00
2 08:00 12:00
1 12:45 17:00
3 13:00 19:30
2 13:00 18:30

That's the way it looks when datasets are missing or surplus:
de.fitsample.timerecording.TimeRecordingRowFixture
day? from? to?
1 12:45 17:00
2 08:00 12:00
2 13:00 18:30
3 13:00 19:30
3 08:00 12:00
4 missing 08:30 16:00
1 surplus 10:00 12:00

So how is this implemented in detail? Let's have a look at our good old
first. The constructor of TimeRecordingColumnFixture creates an instance of TimeRecording and stores it in the TimeRecordingRepository:
    public TimeRecordingColumnFixture() {
timeRecording = new TimeRecording();
TimeRecordingRepository.getInstance().store(timeRecording);
}

Our RowFixture code is found in the classes
public class TimeRecordingRowFixture extends RowFixture {

private TimeRecording timeRecording;

public TimeRecordingRowFixture() {
timeRecording = TimeRecordingRepository.getInstance().retrieve();
}

public Object[] query() throws Exception {
Object[] result = new Object[timeRecording.size()];
for (int i = 0; i < result.length; i++) {
result[i] = new TimeRecordRowFixtureAdapter(timeRecording
.getRecord(i));
}
return result;
}

public Class getTargetClass() {
return TimeRecordRowFixtureAdapter.class;
}

public Object parse(String text, Class type) throws Exception {
if (type == Time.class) {
return Time.valueOf(text);
}
return super.parse(text, type);
}
}

So what's happening here? The constructor does not create any new instance of TimeRecording but retrieves it from the TimeRecordingRepository; and that's the instance we created and set up with data using our ColumnFixture.

The main methods of RowFixture are
  • Object[] query()
  • Class getTargetClass()

Method query() returns the datasets under test, and getTargetClass() the type of a single dataset. Wait a minute, I would have expected getTargetClass() to return TimeRecord as the type of a dataset. What the heck do we need this TimeRecordRowFixtureAdapter for? Well, the RowFixture tries to map the row header names to fields, resp. methods of the dataset class. Our TimeRecord does contain a field named day, but none for from and to since they are encapsulated by a TimeFrame. In order to make these values accessible to the RowFixture we just wrap the TimeRecord with an adapter. This also provides a way to use names more convenient to the testing audience:
public class TimeRecordRowFixtureAdapter {

private TimeRecord timeRecord;

public TimeRecordRowFixtureAdapter(TimeRecord timeRecord) {
this.timeRecord = timeRecord;
}

public int day() {
return timeRecord.getDay();
}

public Time from() {
return timeRecord.getTimeFrame().getStartTime();
}

public Time to() {
return timeRecord.getTimeFrame().getEndTime();
}



< Gui Test with other Fixtures | Links and Books >
< Under the Hood | New Requirements >

 Settings

Testing with FitNesse

Works quite well with FIT, what do I need FitNesse for? One thing is that not everybody likes editing a HTML document; and saving a Word document as HTML does not always provide a usable result. And if the customer likes to run FIT tests itself, she has to cope with the FITRunner command line tool.

Here comes FitNesse into play. FitNesse is a Wiki with its own Web-Server and a built-in FITRunner. You are currently using a FitNesse Wiki. The syntax is quite easy, at least a lot easier than HTML (click the Edit button on the left and have a look). And you can run a test very easily, just by clicking the Test button. Not all customers like Wikis (even if it is becoming popular thanks to projects like WikiPedia). But FitNesse also provides the possibility to import tables from Word and Excel using Copy/Paste.

But enough big words here, let's face the real thing and run a test from our example. Click the Test button. Now!

Subtraction of mandatory breaks

This test checks if mandatory breaks are subtracted.
  • More than 6 hours of work: 1/2 hour
  • More than 9 hours of work: 3/4 hour

de.fitsample.timerecording.TimeRecordingColumnFixture
day from to working time?
1 6:00 8:00 2.0
2 6:00 9:00 3.0
3 6:00 10:00 4.0
4 6:00 11:00 5.0
5 6:00 12:00 6.0
6 6:00 12:01 6.0
7 6:00 12:30 6.0
8 6:00 12:31 6.02
9 6:00 13:00 6.5
10 6:00 14:00 7.5
11 6:00 15:00 8.5
12 6:00 15:30 9.0
13 6:00 15:31 9.0
14 6:00 15:45 9.0
15 6:00 15:46 9.02
16 6:00 16:00 9.25
17 6:00 16:45 10.0
18 6:00 16:46 10.02
19 6:00 17:00 10.25
20 6:00 18:00 11.25



Taken break shorter than mandatory

If the employee has taken a break shorter than mandatory, then the difference is subtracted from the working time.

de.fitsample.timerecording.TimeRecordingColumnFixture
day from to recorded time? recorded break? mandatory break? working time? sum?
1 10:00 12:00 2.0 0 0 2 2
  1 12:15 17:00 6.75 0.25 0.5 6.5 6.5
2 08:00 12:00 4.0 0 0 4 10.5
  2 12:15 18:30 10.25 0.25 0.75 9.75 16.25
3 08:00 12:00 4.0 0 0 4 20.25
  3 12:15 19:30 11.25 0.25 0.75 10.75 27.00


Taken break equals mandatory

If the employee has taken a break that equals the mandatory break, nothing is subtracted.

de.fitsample.timerecording.TimeRecordingColumnFixture
day from to recorded time? recorded break? mandatory break? working time? sum?
1 10:00 12:00 2.0 0.0 0.0 2 2
  1 12:30 17:00 6.5 0.5 0.5 6.5 6.5
2 08:00 12:00 4.0 0.0 0.0 4 10.5
  2 12:45 18:30 9.75 0.75 0.75 9.75 16.25
3 08:00 12:00 4.0 0.0 0.0 4 20.25
  3 12:45 19:30 10.75 0.75 0.75 10.75 27.00


Taken break longer than mandatory

If the break taken by the employee is longer than mandatory, nothing is subtracted.

de.fitsample.timerecording.TimeRecordingColumnFixture
day from to recorded time? recorded break? mandatory break? working time? sum?
1 10:00 12:00 2.0 0.0 0.0 2 2
  1 12:45 17:00 6.25 0.75 0.25 6.25 6.25
2 08:00 12:00 4.0 0.0 0.0 4 10.25
  2 13:00 18:30 9.5 1.0 0.5 9.5 15.75
3 08:00 12:00 4.0 0.0 0.0 4 19.75
  3 13:00 19:30 10.5 1.0 0.75 10.5 26.25


< Under the Hood | New Requirements >