Port of schuchert.wikispaces.com


FitNesse.Tutorials.1

FitNesse.Tutorials.1

Introduction

This tutorial assumes some basic FitNesse knowledge. If you need help installing or running FitNesse, please go here first. In this tutorial, you will use a Decision table to send data into a system and verify results returned. You will:

This tutorial is primarily about getting you over the hurtle of the mechanics of getting tests to execute using FitNesse. Even so, you will see some basic design considerations play out as well.

Note, this tutorial assumes you are running FitNesse on localhost at port 8080 http://localhost:8080. If you are not sure how to do that, try this tutorial.

Background

FitNesse.Slim Decision Tables are a common way to get test data into a System Under Test. A Decision table has three parts (only the first of which is actually required):

Here is an example FitNesse decision table:

|Add Programs To Schedule                                                     |
|name      |episode                      |channel|date     |start time|minutes|
|House M.D.|House Makes Wilson Mad       |7      |5/12/2008|7:00      |60     |
|Doctor Who|The One where He Saves the UK|12     |5/17/2008|8:00      |60     |

The first row names the fixture. In this case, FitNesse will look for a class called AddProgramsToSchedule. The second row lists the column names. FitNesse will look for the following methods in AddProgramsToSchedule:

These methods can all take Strings or some, where there’s a conversion available, other types as well. For example, “setChannel()” could take an int. It is also possible to define your own translations, however this tutorial does not cover that feature.

Finally, there are two data rows. Given the name of the fixture, this table’s goal is to apparently add two programs to the schedule.

Creating this table

Here are some preliminary steps to get this table created (there will be more later, this table is the skeleton of a test):

>DecisionTableExample

Now you can execute the page. Click on the Test button. The tests will fail dues to a missing fixutre. FitNesse will color the first row yellow and add the message “.”. Now you must create a Fixture class and add it to the test page.

Creating the Fixture

If you are planning on using Eclipse and working in Java, then you can get a repository from github: fitnesse-tutorials. Review the instructions here.

Creating a fixture involves:

For full details on these steps, you can review the material here if you’re planning on working in Java or here if you’re planning on working in C#.

Here is one such fixture (in Java) that will get this test to “pass”. Since there are no assertions, this really isn’t a very good test yet, but it does make it easier to get it all green.

package com.om.example.dvr.fixtures;

public class AddProgramsToSchedule {
   public void setName(String name) {
   }

   public void setEpisode(String name) {
   }

   public void setChannel(int channel) {
   }

   public void setDate(String date) {
   }

   public void setStartTime(String startTime) {
   }

   public void setMinutes(int minutes) {
   }
}

There are still a few things you need to do to make the page use this class:

!define TEST_SYSTEM {slim}
!path /Users/schuchert/src/fitnesse-tutorials/DVR/bin
|import|
|com.om.example.dvr.fixtures|

Here’s the updated page put all together(again, update the directory in the !path statement accordingly):

!define TEST_SYSTEM {slim}

!path /Users/schuchert/src/fitnesse-tutorials/DVR/bin

|import|
|com.om.example.dvr.fixtures|

|Add Programs To Schedule                                                     |
|name      |episode                      |channel|date     |start time|minutes|
|House M.D.|House Makes Wilson Mad       |7      |5/12/2008|7:00      |60     |
|Doctor Who|The One where He Saves the UK|12     |5/17/2008|8:00      |60     |

Note: You might need to add the following line as well (e.g., if you built from source):

!path fitnesse.jar

Run the test and verify that the page passes successfully.

While you are at it, you have your original test page from the first tutorial. You can verify it still passes as well.

Add Assertions

Right now, this table does not assert any results, which means the underlying fixture can do the same, which is not much. Let’s extend this just a bit to have the table actually perform validation:

|Add Programs To Schedule                                                              |
|name      |episode                      |channel|date     |start time|minutes|created?|
|House M.D.|House Makes Wilson Mad       |7      |5/12/2008|7:00      |60     |true    |
|Doctor Who|The One where He Saves the UK|12     |5/17/2008|8:00      |60     |true    |

Try running this page and FitNesse will complain that it cannot find the created[0] method. The name is followed by the number of expected parameters, which is 0 in our case. Here is just such a method you can add to your “AddProgramsToSchedule” fixture:

   public boolean created() {
      return true;
   }

Update your table and add the missing method. Verify that the test still passes. You’ll notice there are three successful assertions.

What is this doing?

Adding a column with a ? at the end of its name requires that the fixture have a method with a matching name (remove spaces, use camel casing) with some return value. FitNesse will execute that method and compare its return value to the value in the cell, marking it green or red for matching/not matching. If you happen to have a cell with no value, the return value will be displayed in the cell with a gray coloring.

Make the Assertion have some Value

There’s nothing in the flow of this table that would cause a problem. However, what if we want to make sure adding a program on top of another is not possible? We can do that by adding one more row to the bottom of the table::

|Conflicts |Should not be added          |7      |5/12/2008|7:00      |30     |false   |

This demonstrates a conflict because the third program is on the same channel, date, time as the first.

This is a non-typical use of the Decision table, but it certainly is legitimate. Assuming the slot is already occupied (even partially), this item should not be added to the schedule.

Run it, you should have one failed assertion. Your code will need some way to know that one slot is already used. Here’s one way to accomplish that:

Update AddProgramsToSchedule.java

package com.om.example.dvr.fixtures;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import com.om.example.domain.TimeSlot;

public class AddProgramsToSchedule {
   private static SimpleDateFormat dateFormat = new SimpleDateFormat("M/d/yyyy|h:mm");
   private List<TimeSlot> scheduledTimeSlots = new LinkedList<TimeSlot>();
   private int channel;
   private String date;
   private String startTime;
   private int minutes;

   public void setName(String name) {
   }

   public void setEpisode(String name) {
   }

   public void setChannel(int channel) {
      this.channel = channel;
   }

   public void setDate(String date) {
      this.date = date;
   }

   public void setStartTime(String startTime) {
      this.startTime = startTime;
   }

   public void setMinutes(int minutes) {
      this.minutes = minutes;
   }

   public boolean created() {
      TimeSlot timeSlot = new TimeSlot(channel, buildStartDateTime(), minutes);

      if (conflictsWithOtherTimeSlots(timeSlot))
         return false;

      scheduledTimeSlots.add(timeSlot);
      return true;
   }

   private boolean conflictsWithOtherTimeSlots(TimeSlot timeSlot) {
      for (TimeSlot current : scheduledTimeSlots)
         if (current.conflictsWith(timeSlot))
            return true;

      return false;
   }

   private Date buildStartDateTime() {
      try {
         String dateTime = String.format("%s|%s", date, startTime);
         return dateFormat.parse(dateTime);
      } catch (ParseException e) {
         throw new RuntimeException("Unable to parse date/time", e);
      }
   }
}

Create new Class: TimeSlot.java

Notice, this class is in a different package (com.om.example.dvr.domain).

package com.om.example.dvr.domain;

import java.util.Date;

public class TimeSlot {

   public final int channel;
   public final Date startDateTime;
   public final int durationInMinutes;

   public TimeSlot(int channel, Date startDateTime, int durationInMinutes) {
      this.channel = channel;
      this.startDateTime = startDateTime;
      this.durationInMinutes = durationInMinutes;
   }

   public boolean conflictsWith(TimeSlot other) {
      if (channel == other.channel && startDateTime.equals(other.startDateTime))
         return true;
      return false;
   }
}

Make these changes to your code and see that your tests now pass. Now your fixture is recording the time slots in use. The implementation of “TimeSlot.conflictsWith” may seem inadequate, but it is complete for what we are testing, so in fact is it fine.

Another issue is that the “AddProgramsToSchedule” class is starting to get somewhat big. Fixtures are enabling technology and as such should primarily handle data translation and then delegate to production code.

Along those lines, “buildStartDateTime” also exhibits feature envy. The “Schedule” is currently just a “List", but it might warrant its own class. While this tutorial's focus is [FitNesse](http://fitnesse.org/), this fixture contains business logic. You do not want any business logic in your fixture code, so that's the next thing to fix.

To fix this, we can introduce a new class and perform some basic re-factoring:

Schedule.java

package com.om.example.dvr.domain;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;

public class Schedule {
   private List<TimeSlot> scheduledTimeSlots = new LinkedList<TimeSlot>();

   public void addProgram(String programName, String episodeName, int channel,
         Date startDateTime, int lengthInMinutes) {

      TimeSlot timeSlot = new TimeSlot(channel, startDateTime, lengthInMinutes);

      if (conflictsWithOtherTimeSlots(timeSlot))
         throw new ConflictingProgramException();

      scheduledTimeSlots.add(timeSlot);
   }

   private boolean conflictsWithOtherTimeSlots(TimeSlot timeSlot) {
      for (TimeSlot current : scheduledTimeSlots)
         if (current.conflictsWith(timeSlot))
            return true;

      return false;
   }
}

ConflictingProgramException.java

package com.om.example.dvr.domain;

public class ConflictingProgramException extends RuntimeException {
   private static final long serialVersionUID = 1L;
}

Updated: AddProgramsToSchedule.java

package com.om.example.dvr.fixtures;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import com.om.example.dvr.domain.ConflictingProgramException;
import com.om.example.dvr.domain.Schedule;

public class AddProgramsToSchedule {
   static SimpleDateFormat dateFormat = new SimpleDateFormat("M/d/yyyy|h:mm");
   private Schedule schedule = new Schedule();
   private int channel;
   private String date;
   private String startTime;
   private int minutes;
   private String programName;
   private String episodeName;

   public void setName(String name) {
      this.programName = name;
   }

   public void setEpisode(String name) {
      this.episodeName = name;
   }

   public void setChannel(int channel) {
      this.channel = channel;
   }

   public void setDate(String date) {
      this.date = date;
   }

   public void setStartTime(String startTime) {
      this.startTime = startTime;
   }

   public void setMinutes(int minutes) {
      this.minutes = minutes;
   }

   public boolean created() {
      try {
         schedule.addProgram(programName, episodeName, channel, buildStartDateTime(),
               minutes);
      } catch (ConflictingProgramException e) {
         return false;
      }
      return true;
   }

   private Date buildStartDateTime() {
      try {
         String dateTime = String.format("%s|%s", date, startTime);
         return dateFormat.parse(dateTime);
      } catch (ParseException e) {
         throw new RuntimeException("Unable to parse date/time", e);
      }
   }
}

This split makes more sense:

This really was just an Extract class refactoring or wrapping a collection. Wrapping collections is generally a good idea. For more details, see the sidebar, Wrapping Collections.

Before moving on, make sure your test passes. Assuming it does, congratulations on a successful refactoring.

Deleting Something By Key

We should be able to add a program, remove it and then add another at the same time slot. Here’s just such a test and it uses something you might have noticed in the first tutorial:

|Add Programs To Schedule                                                                      |
|name      |episode                      |channel|date     |start time|minutes|created?|lastId?|
|House M.D.|House Makes Wilson Mad       |7      |5/12/2008|7:00      |60     |true    |$p=    |
|Doctor Who|The One where He Saves the UK|12     |5/17/2008|8:00      |60     |true    |       |
|Conflicts |Should not be added          |7      |5/12/2008|7:00      |30     |false   |       |

This introduces another column, lastId?. The implementation, which is below, simply returns the last id stored in the method created(). The definition is simply: (:), e.g., the id's above are:

Update your table with the new table above and try running this page and FitNesse will complain that it cannot find the lastId[0] method. The name is followed by the number of expected parameters, which is 0 in our case. Here is just such a method you can add to your “AddProgramsToSchedule” fixture:

public String lastId() {
   return lastId;
}

Add the missing method. Verify that the test still passes. You’ll notice there are three unsuccessful assertions for “lastId”.

As for the third id, you’ll see that in a minute. To get this to run, you’ll need to make several changes:

Add: Program.java

package com.om.example.dvr.domain;

public class Program {

   public final String programName;
   public final String episodeName;
   public final TimeSlot timeSlot;

   public Program(String programName, String episodeName, TimeSlot timeSlot) {
      this.programName = programName;
      this.episodeName = episodeName;
      this.timeSlot = timeSlot;
   }

   public String getId() {
      return String.format("(%s:%d)", programName, timeSlot.channel);
   }
}

Update: Schedule.java

package com.om.example.dvr.domain;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;

public class Schedule {
   private List<Program> scheduledPrograms = new LinkedList<Program>();

   public Program addProgram(String programName, String episodeName, int channel,
         Date startDateTime, int lengthInMinutes) {

      TimeSlot timeSlot = new TimeSlot(channel, startDateTime, lengthInMinutes);

      if (conflictsWithOtherTimeSlots(timeSlot))
         throw new ConflictingProgramException();

      Program program = new Program(programName, episodeName, timeSlot);
      scheduledPrograms.add(program);
      return program;
   }

   private boolean conflictsWithOtherTimeSlots(TimeSlot timeSlot) {
      for (Program current : scheduledPrograms)
         if (current.timeSlot.conflictsWith(timeSlot))
            return true;

      return false;
   }
}

Update: AddProgramsToSchedule.java

package com.om.example.DVR.fixture;

import java.util.Calendar;
import java.util.Date;

import com.om.example.dvr.domain.ConflictingProgramException;
import com.om.example.dvr.domain.Program;
import com.om.example.dvr.domain.Schedule;

public class AddProgramsToSchedule {
   // snip
   private String lastId;

   // snip

   public boolean created() {
      try {
         Program p = schedule.addProgram(programName, episodeName, channel,
               buildStartDateTime(), minutes);
         lastId = p.getId();
      } catch (ConflictingProgramException e) {
         return false;
      }
      return true;
   }

   public String lastId() {
      return lastId;
   }
   // snip
}

Once you’ve made these updates, execute the table. You should notice three values in the “lastId?” column:

In all cases:

In the first case, there is a variable assignment, which FitNesse dutifully assigned.

This variable is available for the rest of the page. However, before we get to that we do have a problem. The lastId? is set upon a successful program add, but it is not reset if the program is not added. Here is a quick fix to improve that:

AddProgramsToScheule.created

   public boolean created() {
      try {
         Program p = schedule.addProgram(programName, episodeName, channel,
               buildStartDateTime(), minutes);
         lastId = p.getId();
      } catch (ConflictingProgramException e) {
         lastId = "n/a";
         return false;
      }
      return true;
   }

Make the update and then you’ll notice the third data row of the lastId? column is now n/a (in gray).

Finally, Delete by Key

Time to add another table and fixture:

|Remove Program By Id|$p|

|Add Programs To Schedule                                                 |
|name   |episode            |channel|date     |start time|minutes|created?|
|Ok now |No longer conflicts|7      |5/12/2008|7:00      |30     |true    |

Just add this to the bottom of your page. You’ll have to create a new fixture. Here is that code:

package com.om.example.dvr.fixtures;

public class RemoveProgramById {
   public RemoveProgramById(String id) {
   }
}

This fixture does not do anything yet, but even so there are several things worthy of note:

The second decision table using the AddProgramsToSchedule fixture on the page should verify that we can add a program to that time slot that was previously occupied.

What to do:

When you run your tests, do you notice a problem? The tests pass! Maybe you expected the second attempt to add would fail, but it appears to work. This illustrates something FitNesse does; each table causes a new instance of the fixture to be created, even on the same page. How can you tell this? If you want to verify it, you could simply add a print statement to the constructor and view the output. I’ve already done that. Here’s the print statement:

Example: Added to AddProgramsToSchedule fixture

   private static int numberCreated = 0;

   public AddProgramsToSchedule() {
      System.out.printf("Creating ProgramsToSchedule #%d\n", ++numberCreated);
   }

Adding this and then executing the tests, FitNesse will display a yellow triangle with the label “Output Captured”. Clicking on that triangle, you’ll see the output captured during test execution::

Standard Output:

Creating ProgramsToSchedule #1
Creating ProgramsToSchedule #2

So what is the problem? The fixture holds the schedule. Each fixture has its own schedule. We need the schedule to be a single instance. You have several options:

Ultimately, how you should do it depends on your system. If your system will eventually need objects like this configured, wired and passed around, then it might make sense to introduce Spring or maybe even a hand-rolled IoC container (a factory of some kind). For our purposes, simply making the schedule static in AddProgramsToSchedule will work effectively. So do that and then see the test fail (note, I’ve removed the constructor and static variable numberCreated in my version to get rid of output making its way into my test execution).

Now that the test is failing, we need a way to get access to the schedule between fixtures. For now, adding a getSchedule() method on the AddProgramsToSchedule fixture is adequate:

public class AddProgramsToSchedule {
   private static Schedule schedule = new Schedule();

   public static Schedule getSchedule() {
      return schedule;
   }

   // snip
}

Now that we have a single Schedule and access to it, we can simply update the constructor in RemoveProgramById to call the code:

package com.om.example.dvr.fixtures;

public class RemoveProgramById {
   public RemoveProgramById(String id) {
      AddProgramsToSchedule.getSchedule().removeProgramById(id);
   }
}

Of course, this requires we add a new method to Schedule:

import java.util.Iterator;

   public void removeProgramById(String programIdToRemove) {
      for (Iterator<Program> iter = scheduledPrograms.iterator(); iter.hasNext();)
         if (iter.next().getId().equals(programIdToRemove)) {
            iter.remove();
            break;
         }
   }

Run your tests and you should see all tests green.

Not Doing the Work in the Constructor

If for some reason, you do not like to do the actual work done in the constructor, you can optionally write the table as follows:

|Remove Program By Id|
|id                  |
|$p                  |

Then you’ll need to update your RemoveProgramByIdFixture as follows:

package com.om.example.dvr.fixtures;

public class RemoveProgramById {
   private String id;

   public RemoveProgramById() {
   }

   public RemoveProgramById(String id) {
      this.id = id;
      execute();
   }

   public void setId(String id) {
      this.id = id;
   }

   public void execute() {
      AddProgramsToSchedule.getSchedule().removeProgramById(id);
   }
}

Note that this Fixture, as written, supports both styles. The real reason I wanted to include this last example was to demonstrate how you can cause a row of a decision table to be executed without include a column with a ? in its name. You add a method called execute(). FitNesse will call that method, if it exists, after calling the last setter (the columns without ? in their name).

Conclusion and Summary

Congratulations, you’ve completed this tutorial.

This tutorial emphasizes Decision tables. There is still more to you can do with decision tables, but this covers most of what you’ll need to know to effectively use decision tables. If you go to your fitness installation and go to FitNesse.SliM.DecisionTable (http://localhost:8080/FitNesse.SliM.DecisionTable), you can read the FitNesse-provided documentation.

However, you’ve learned several things in this tutorial:

After working with decision tables, the next tutorial which makes sense is this one on query tables. <–back Next Tutorial–>


Comments

" Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.