devxlogo

Apply Fit and FitNesse to Run Web-Based Acceptance Tests

Apply Fit and FitNesse to Run Web-Based Acceptance Tests

nit testing is a well-known practice that helps developers validate the functionality of components and identify bugs early when they are easy to fix. By leveraging frameworks like JUnit, unit tests are very easy to write and execute, and therefore provide you with very rapid feedback about the status of the system. The problem with unit tests by themselves is that they test such fine-grained pieces of functionality that it is easy to end up with gaps in coverage for the system overall. And, because unit tests are strictly a developer tool, any miscommunication or misunderstanding about requirements cannot be caught, and the business users have no visibility into these tests.

Acceptance tests, on the other hand, test coarse-grained services within the application, which complements the coverage gained by unit tests. The same people who write acceptance tests also generally write and maintain the requirements; therefore, the tests usually reflect the requirements more accurately.

Creating automated acceptance tests as a part of requirements definition has significant benefits. First, regression testing becomes much easier when all the requirements for the system are backed by automated tests, which makes finding and fixing defects much less costly. In addition, developers have a much easier time implementing features when there is a tangible example that demonstrates how the system should function. Writing programmatic tests at the service level can improve the architecture of the application being developed. Generally speaking, an application that is easy to test is also well designed. If it’s difficult to write tests at the service level, it could indicate that the system is too tightly coupled or that the services being tested are not well encapsulated.

Despite the benefits of writing and running acceptance tests early, this type of testing is rarely done until late in the project’s life cycle. Why? The problem with these tests is that they are generally very expensive to write and automate. The people defining the acceptance criteria, the customers or business analysts as their proxies, usually aren’t programmers and don’t have the skills or time necessary to write effective automated test scripts.

The Framework for Integrated Test (Fit) and FitNesse address these issues. Fit is an open source acceptance testing framework, and FitNesse is a wiki-based test execution engine that allows users to write, edit, and run Fit tests over the web. Together these tools enable developers and users to express acceptance tests in a tabular format and execute them from a simple web interface. Using Fit and FitNesse, teams can reduce drastically the effort required to create automated acceptance tests, all while increasing the accessibility of the tests and their results.

Physical FitNesse
A FitNesse test is composed of three basic elements: the test page, the test fixture, and the execution engine. The first component, the test page, is simply a wiki page that contains one or more test tables in addition to any nonexecutable tables or text that describe the test or requirements in more detail. When FitNesse executes a test page, the test tables are run sequentially, which means that not only can you put multiple test tables on a given page but you can also have subsequent test tables rely on the state that was set up by previous tables.

The second component, the test fixture, is a class that interprets the information provided in a table and uses it to exercise the application. The examples discussed here demonstrate using Java to write the fixture code; however, there is also a version of FitNesse included in the main distribution that runs on the .NET framework, and it would be relatively trivial to port any of these examples to C# or VB.Net.

All fixture classes ultimately extend the base Fixture class provided in the FitNesse libraries. In addition to the base Fixture class there are a number of fixture subclasses that can be used as extension points that provide additional functionality and make writing the fixtures for common types of test tables very easy. The vast majority of test fixtures that the average team must write will extend from one of these subclasses.

The execution engine is the third component that is provided by the FitNesse libraries and is completely abstracted away from the users of the tool.

Testing Business Rules
FitNesse is open source and freely available for download. You can get the instructions for downloading and installing the FitNesse server on your own machine. To begin working with Fit take a look at a test table example (see Table 1).

Table 1. Test Table: Put business rules for a car rental business to the test.

com.devx.fit.RentalRates
agecar typebooking methodrental allowed?daily rate?
25EconomyPhoneTrue15.00
25EconomyInternetTrue13.50
24EconomyPhoneTrue16.50
40StandardPhoneTrue20.00
25StandardInternetTrue18.00
30ExoticPhoneTrue45.00
29ExoticPhoneFalse
17StandardInternetFalse
18StandardInternetTrue20.00

Table 1 captures several test cases related to a set of business rules that a car rental company might use for determining whether an individual is eligible to rent a vehicle, and if so what the rental rate should be. In this case the business rules are:

  • Customers who book their rental online receive a 10 percent discount.
  • Customers under 25 years old are subject to a 10 percent fee.
  • Exotic cars can be rented only by customers 30 years old or older.
  • Customers under 18 years old are not allowed to rent any car.

The table lists several sets of data, as an example, and the expected output for two calculations. This table alone might not communicate completely the business rules the system is required to enforce, but when it is used to augment the English description it does a very good job at removing ambiguity from the requirements. For instance, these test cases clearly show that an exotic car can be rented by a 30-year-old driver, but not by someone who is 29 years old. Based solely on the English description of the rules, this requirement may not have been completely clear. Also, the rate calculation for an underage driver who books his or her rental through the Internet is not immediately clear based on the given rules. Should the online discount be determined based on the original rate or the rate including the 10 percent fee? The test table clearly shows that the former is the correct calculation.

Linking Test Tables
The test table shown in Table 1 is useful as a communication tool; however, the real power of Fit comes from the ability to execute these tables as tests against the application itself. To run any table as a test you must write a small amount of code in the form of a Fixture class. Here is the Fixture class that supports Table 1:

package com.devx.fit;import com.devx.fit.app.BookingMethod;import com.devx.fit.app.RateCalculator;import fit.ColumnFixture;public class RentalRates extends ColumnFixture {  public int age;  public String carType;  public BookingMethod bookingMethod;  private RateCalculator calculator = new RateCalculator();  public Boolean rentalAllowed() {    return calculator.isRentalAllowed(age, carType);  }  public double dailyRate() {    return calculator.calculateRate(age, carType, bookingMethod);  }  @Override  public Object parse(String s, Class type) throws Exception {    if (type == BookingMethod.class) {      return BookingMethod.valueOf(s.toUpperCase());    }    return super.parse(s, type);  }}

Note that this class extends from the ColumnFixture class, which is a subclass of Fixture provided in the Fit libraries and does the work of binding the data from the table to the fields and methods in your class. When writing a ColumnFixture class all you have to do is provide a public field for each input column in your table and a public() method for each output column.

When you execute the test for this table the fixture will iterate over each row of the table, binding each input field to a public instance variable. This binding involves converting the value in the table into the necessary type and reflectively setting the field to the converted value. After the input fields have been set, the ColumnFixture executes the public() method that matches the name of each output column and compares the return value to the value from the table. If all of the return values match the expected values, the row will be displayed in green; if there are any that don’t match, the row will turn red.

The base classes provided by Fit do automatic conversions from the strings provided in a test table to the types needed by the Fixture class for setting fields and doing comparisons on the return values of methods. Fit has a default conversion implementation for all the primitive types, their wrapper classes, the String, Date, and ScientificDouble classes, as well as arrays of any of these types. If, however, you wish to define a custom conversion for these or any other classes, you can do so by overriding the parse() method of your fixture. This override is demonstrated in the RentalRates fixture class.

Note that a custom parse() method had to be implemented because the car type field is a custom type that isn’t supported by Fit. It uses the static CarType.valueOf() method to convert from a string into an instance of the typesafe enumeration. You can follow a similar pattern for defining a mapping from a string to any Java class.

FitNesse Test
You need to create a new page called TestExample to run the test table in FitNesse. Simply click the Edit Locally link on the left side of the FitNesse front page, scroll to the bottom of the text area, and type the wiki word TestExample. Then save the page. You will return to the front page, and the TestExample link you created will appear at the bottom with a small question mark next to it. Click the question mark to create the new page. You’ll switch immediately to the new TestExample page’s edit view. Add this wiki markup to the page body, and click Save:

!path fitnesse.jar!path [Path to fitnesse-examples.jar]!|com.devx.fit.RentalRates||age|car type|booking method|rental allowed?|daily rate?||25 |Economy |Phone         |True           |15.00      ||25 |Economy |Internet      |True           |13.50      ||24 |Economy |Phone         |True           |16.50      ||40 |Standard|Phone         |True           |20.00      ||25 |Standard|Internet      |True           |18.00      ||30 |Exotic  |Phone         |True           |45.00      ||29 |Exotic  |Phone         |False          |           ||17 |Standard|Internet      |False          |           ||18 |Standard|Internet      |True           |20.00      |

The first two lines of this wiki code example configure the classpath that FitNesse will use to run this test page. You should modify the second line to give the fully qualified path to the JAR example that is provided. Alternatively, you can also download the source for the examples and set the path element to point to the target directory of the provided build. This procedure is very useful when running FitNesse locally, as changes made in your workspace will be reflected immediately when running tests.

Because the name of this page begins with the word “Test,” a test link will appear at the top of the navigation bar on the right. Click the test link, and FitNesse will execute the table and report the results.

As you can see from this example, FitNesse uses the pipe character to represent cell boundaries and new lines to delimit rows. For the sake of readability the cells in this example have been padded with spaces to force the columns to line up properly, but this additional white space is completely optional and will be ignored when the page is rendered.

Note also that the first row of the test table is preceded with an exclamation point that marks the table as a test and tells Fit to execute it using the Fixture class defined in the first row.

Using RowFixture Classes
Previously, a ColumnFixture table was used to define the expected output of an operation, given a set of input. This style of table works well for testing things like calculation and validation logic. However, there are other scenarios where a given operation might return more than one result. In these cases the RowFixture class can be extended to easily validate a list of data. The Table 2 example asserts that a given set of records is returned when generating the High Risk Rentals report.

Table 2. Risk Taking: Generating the report for this test case asserts that a given set of records is returned.
com.devx.fit.HighRiskReport
namerisk factor
MattExotic
AmyUnderage
JimUnderage
JoeExotic

The test table shown in Table 2 is fairly straightforward. Each row represents one record in the report. If there are missing or additional records, or if any of the records contain incorrect data, this test will fail.

Here is the Fixture code that is needed to execute the test table shown in Table 2:

package com.devx.fit;import java.util.ArrayList;import java.util.Collection;import com.devx.fit.app.RentalRecord;import com.devx.fit.app.ReportingService;import fit.RowFixture;public class HighRiskReport extends RowFixture {  private ReportingService reportingService = new ReportingService();  @Override  public Class getTargetClass() {    return ReportEntry.class;  }  @Override  public Object[] query()throws Exception {    Collection report = reportingService.getHighRiskRentals();    ArrayList entries = new ArrayList();    for (RentalRecord record : report) {      entries.add(new ReportEntry(record.getName(), record.getRiskFactor()));    }    return entries.toArray();  }  public static class ReportEntry {    public String name;    public String riskFactor;    public ReportEntry(String name, String riskFactor) {      this.name = name;      this.riskFactor = riskFactor;    }  }}

As you can see, a RowFxture simply defines a query() method that returns an array of objects and a getTargetClass() method that returns the type of the objects Fit can expect to find in that array. The type returned by getTargetClass() should contain public fields corresponding to each column of the test table that Fit will use to do its comparisons when the test is run.

By itself, the table shown in Table 2 doesn’t really make much sense and won’t execute successfully because we don’t know where the data that backs this report come from. To give context to this test and prime the system with the necessary data we can use a third fixture type called a RowEntryFixture. A RowEntryFixture is a modified type of ColumnFixture where instead of asserting on the output of a given operation, the Fixture class simply implements a method that knows how to handle each row.

The test table shown in Table 3 and its supporting Fixture code is an example of a RowEntry-style table that could be used to set up the data needed to successfully run the previous high-risk report table (see Table 2).

Table 3. RowEntry Style: This test sets up the data to run the same high-risk rentals report as was run for the data in Table 2.
com.devx.fit.Rent
nameagecar type
John34Economy
Chris53Standard
Matt42Exotic
Amy24Economy
Jim19Standard
Joe39Exotic
package com.devx.fit;import com.devx.fit.app.RentalService;import fitnesse.fixtures.RowEntryFixture;public class Rent extends RowEntryFixture {  public String name;  public int age;  public String carType;  private RentalService rentalService = new RentalService();  @Override  public void enterRow()throws Exception {    rentalService.rent(name, age, carType);  }}

Like a ColumnFixture, a RowEntryFixture processes each row by first binding all of the columns in the table to public instance fields. After the fields are bound, the fixture calls the enterRow() method to perform the desired operation. The only way for a RowEntry table to fail is if the enterRow() method throws an exception. In this case, the row or rows that threw exceptions during processing will be shown in red.

To use the Rent and HighRiskReport fixtures together, the two tables must appear on the same test page in the correct order. When FitNesse executes a test page it runs each test table found on the page in succession and uses a single classloader per page. The sample application is therefore able to use a singleton as a data store, and all the records entered by the Rent table will be available when the HighRiskReport table executes.

As you have seen, Fit and FitNesse can be used to easily create a suite of automated acceptance tests that can be shared and maintained by an entire team. While the fixtures and table styles discussed here are the ones most commonly used, combined they are just the tip of the iceberg. The libraries provided by Fit and FitNesse are very extensible and allow you to create fixtures that can execute any table as a test. By using these tools on your project you can reap all the benefits of acceptance testing at a very low cost.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist