http://www.developer.com/

Back to article

Writing a Simple Automated Test in FitNesse


December 15, 2006

You want to verify application functionality by using FitNesse. The application might be a web application, a web services API, a desktop UI, or something else. For the example, you'll simplify things and verify against a simple Java API.

Dive in! First, the application. You've built some code to track late fees on checked-out books in a library system. The core class, Checkout, appears in Listing 1.

Listing 1. Checkout.java.

package com.langrsoft.app;

import java.util.*;

public class Checkout {
   private Date checkoutDate;
   private Material material;
   private Date returnDate;

   public Checkout(Material material, Date checkoutDate) {
      this.material = material;
      this.checkoutDate = checkoutDate;
   }

   public Material getMaterial() {
      return material;
   }

   public Date getCheckoutDate() {
      return checkoutDate;
   }

   public boolean isReturned() {
      return returnDate != null;
   }

   public Date getReturnDate() {
      return returnDate;
   }

   public void returnOn(Date date) {
      returnDate = date;
   }

   public int daysLate() {
      return DateUtil.daysAfter(getDueDate(), returnDate);
   }

   public Date getDueDate() {
      return DateUtil.addDays(checkoutDate,
         material.getCheckoutConstraints().getPeriodAsDays());
   }

   public boolean isInGracePeriod() {
      return daysLate() <=
         material.getCheckoutConstraints().getGracePeriod();
   }

   public int amountToFine() {
      if (isInGracePeriod())
         return 0;
      return daysLate() *
         material.getCheckoutConstraints().getCentsPerDay();
   }
}

Some of the supporting classes and interfaces appear in Listing 2. The function of methods in the DateUtil class, not shown, should be fairly obvious. All code, including JUnit tests, is available for download—check the link at the end of this article.

Listing 2. Supporting classes.

package com.langrsoft.app;

public interface Material {
   CheckoutConstraints getCheckoutConstraints();
}

// --------------------------------------

package com.langrsoft.app;

public interface CheckoutConstraints {
   int getPeriodAsDays();
   int getGracePeriod();
   int getCentsPerDay();
}

// --------------------------------------

package com.langrsoft.app;

public class Book implements Material {
   protected static final int BOOK_CHECKOUT_PERIOD = 21;
   protected static final int BOOK_GRACE_PERIOD    =  3;
   protected static final int BOOK_FINE            = 10;

   public CheckoutConstraints getCheckoutConstraints() {
      return new CheckoutConstraints() {

         public int getCentsPerDay() {
            return BOOK_FINE;
         }

         public int getGracePeriod() {
            return BOOK_GRACE_PERIOD;
         }

         public int getPeriodAsDays() {
            return BOOK_CHECKOUT_PERIOD;
         }};
   }
}

The code shows that you construct a Checkout object with a material (book, movie, and so forth) and a checkout date. The customer paying for your library system wants to ensure that you're correctly calculating fines for materials returned late.

Late fine calculation is reasonably easy. Materials have a checkout period that represents the number of days a patron can borrow a material before it's late. The system obtains the due date by adding the checkout period to the checkout date. If the number of days late is less than or equal to the grace period, there is no fine. Otherwise, the fine is the number of days late times the daily fine amount.

You want to script some tests so that you can prove your system works—both for yourself and for your customer. To satisfy your customer, you'll need these scripts to express things clearly. You'll use FitNesse to accomplish these goals.

To store the test, you create a new FitNesse page, TestCheckout. By naming the page starting with the word "Test," FitNesse knows to recognize your new page as a test page. Upon saving TestCheckout, an additional button marked Test appears in the left-hand margin. If you forget to name a page appropriately, you can always click the Properties button in the left-hand margin and click on the Test checkbox.

You edit the contents of TestCheckout so that it contains the following:

!path c:Fitnessefitnesse.jar
!path C:Documents and SettingsjlangrMy Documentsworkspace
         gamelanLibrarybin
!path C:Documents and SettingsjlangrMy Documentsworkspace
         gamelanLibraryFixturesbin

!|fixtures.BookRules|
|daily fine?|grace period?|checkout period?|
|10|3|20|

The first three !path lines add to the FitNesse classpath. FitNesse needs to know where to look for test fixtures, which are bits of Java code you will create that interface with the Checkout application. It also needs to know the location of the FitNesse library itself, as well as the location of the application. In an integration environment, these usually would be references to JAR files. (You will need to change the path statements to reflect appropriate locations on your machine.)

The second set of lines represents a test table, also known as a FIT (Framework for Integrated Tests) table. The pipe or bar symbol, |, separates columns in the table. The ! prior to the table escapes the table, so that FitNesse does not interpret any of its contents as wiki words.

The first line of the table is the fixture name, fixtures.BookRules. A fixture is an intermediary between the FIT table and the application you're testing. You will be developing your fixtures in Java, although it's possible to develop fixtures in other languages, such as C#. The fixture name corresponds to the fixture class name, so you must code the fixture in the fixtures.BookRules class.

The second line of the table represents column headers. You have three headers: daily fine?, grace period?, and checkout period?. Column header names ending with a ? are queries, so all of your columns represent queries. The goal of a query column is to extract some information from the application (via the fixture code), and verify that it matches data in the column.

The third line of the table represents an actual row of data. For each row in a table, FitNesse will make calls out to the fixture code. Here, FitNesse will ask the fixture for each of daily fine, grace period, and checkout period, and compare it against the expected values of 10, 3, and 20, respectively.

The fixture code appears in Listing 3.

Listing 3. fixtures.BookRules.

package fixtures;

import com.langrsoft.app.*;

import fit.*;

public class BookRules extends ColumnFixture {
   public int dailyFine() {
      return new Book().getCheckoutConstraints().getCentsPerDay();
   }

   public int gracePeriod() {
      return new Book().getCheckoutConstraints().getGracePeriod();
   }

   public int checkoutPeriod() {
      return new Book().getCheckoutConstraints().getPeriodAsDays();
   }
}

The class fixtures.BookRules extends from the class fit.ColumnFixture, which can be found in fitlibrary.jar. You'll need this JAR on your classpath to compile fixtures.BookRules.

Each column header in the fixture must map to a public method in BookRules. FitNesse concatenates words in the column header name, appropriately adjusting capitalization to meet Java camel case naming conventions. For example, the column header "daily fine" maps to the query method dailyFine().

The one line of code within dailyFine() interacts with code in the library system. Here, you're testing Java classes directly, but fixture code might also access a system through an interface layer such as a web service.

Now, try out the test. You click the Test button located in the left-hand margin. The results are shown in Figure 1. (If you see yellow, something is configured improperly. Double-check your path statements. Refer to the FitNesse online user's guide. Send me an email. Or post a message to the FitNesse Yahoo! group.)



Click here for a larger image.

Figure 1. FitNesse execution.

The green cells indicate that an expected cell value matched what the fixture code actually returned. The red cell beneath the column header checkout period? indicates an error—the test expected 20, but the system returned 21. You know the system is right in this case, so you edit the FitNesse page and change 20 to 21. You rerun the test by clicking the Test button. You see all green, and a summary showing "Assertions: 3 right, 0 wrong, 0 ignored, 0 exceptions."

Take a quick look at an additional fixture, one that can verify the late fine calculations. You'll create a test table to use a new fixture named fixtures.CheckinBook, You edit the test page, updating its contents to look like Listing 4.

Listing 4. An additional fixture.

!path c:Fitnessefitnesse.jar
!path C:Documents and SettingsjlangrMy Documentsworkspace
         gamelanLibrarybin
!path C:Documents and SettingsjlangrMy Documentsworkspace
         gamelanLibraryFixturesbin

!|fixtures.BookRules|
|daily fine?|grace period?|checkout period?|
|10|3|21|

!|fixtures.CheckinBook|
|checkout date|due date?|checkin date|daysLate?|fine?|
|12/1/2006|12/22/2006|12/22/2006|0|0|
|12/1/2006|12/22/2006|12/23/2006|1|0|
|12/1/2006|12/22/2006|12/25/2006|3|0|
|12/1/2006|12/22/2006|12/26/2006|4|40|
|12/1/2006|12/22/2006|12/27/2006|5|50|

The new table verifies the due date, days late, and fine amount for a book, given its checkout date and return date. Note that two of the column headers, checkout date and checkin date don't end with a ?. They're not queries; they instead represent input data elements. FitNesse uses data in these "setter" columns to initialize a public field in the fixture.

The fixtures.CheckinBook table in listing 4 contains five data rows that cover a number of data scenarios. When you click the Test button, FitNesse calls out to the fixture five times, once for each row in the table. For each row, FitNesse first initializes public fields in the corresponding fixture using each of the setter columns. For example, FitNesse sets the public field checkoutDate to the value 12/1/2006 for each row in the table. Once all setters execute, FitNesse executes each of the query methods.

The corresponding fixture code appears in Listing 5.

Listing 5. fixtures.CheckinBook.

package fixtures;

import java.util.*;

import com.langrsoft.app.*;

import fit.*;

public class CheckinBook extends ColumnFixture {
   public Date checkoutDate;
   public Date checkinDate;

   public Date dueDate() {
      Checkout checkout = new Checkout(new Book(), checkoutDate);
      checkout.returnOn(checkinDate);
      return checkout.getDueDate();
   }

   public int daysLate() {
      Checkout checkout = new Checkout(new Book(), checkoutDate);
      checkout.returnOn(checkinDate);
      return checkout.daysLate();
   }

   public int fine() {
      Checkout checkout = new Checkout(new Book(), checkoutDate);
      checkout.returnOn(checkinDate);
      return checkout.amountToFine();
   }
}

For each query method that executes, remember that FitNesse has already populated the public fields checkoutDate and checkinDate.

You run the test page to ensure everything passes.

As a programmer, you might balk at the design of the fixture. The fixture code creates a checkout object and calls its returnOn method three separate times, once for each of the three query methods. That's not very efficient!

The important point is that the test tables are an expression of the requirements of the system. They might not represent how you think users should interact with the system. Yet, that's how the customer who designed the test tables wants to think about things.

The test tables are your focal point for future negotiation. You can center debate and discussion around these tables. You might get the customer to agree to split the fixtures.CheckinBook table into two. Or, you might realize that you need to redesign the APIs into the system. Even better, going forward, you can consider that the FitNesse tests can help drive the development of the system.

You've just scratched the surface in terms of FitNesse capabilities. There are numerous fixture types, including a RowFixture type that allows you to verify collections of data. FitNesse also provides many ways to help make your tests more expressive. Read through the online users' guide and experiment with the FitNesse capabilities that it explains. You might also check out the book, Fit For Developing Software, by Rick Mugridge.

Download the Code

You can download the code that accompanies this article here.

About the Author

Jeff Langr is a veteran software developer with a score and more years of experience. He's authored two books and dozens of published articles on software development, including Agile Java: Crafting Code With Test-Driven Development (Prentice Hall) in 2005. You can find out more about Jeff at his site, http://langrsoft.com, or you can contact him directly at jeff@langrsoft.com.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date