FitNesse: Wiki-Based Acceptance Testing and ATDD

FitNesse: Wiki-Based Acceptance Testing and ATDD

FitNesse is a self-contained wiki server and acceptance testing framework. Teams write test tables directly in a browser-based wiki, and FitNesse executes them against Java (or other language) fixtures. It is the canonical tool for Acceptance Test-Driven Development (ATDD) and is especially well-suited to legacy Java systems and financial or regulatory environments where non-engineers must own test authoring.

Key Takeaways

FitNesse is a wiki that runs tests. You open a browser, navigate to a wiki page, write a table, press "Test", and see green or red — no IDE, no file system, no build required.

SLiM is the modern execution engine. SLiM (Simple List Invocation Method) replaced FIT and is what new projects should use; it is simpler, more reliable, and easier to extend.

Decision Tables map inputs to expected outputs. They are the closest FitNesse equivalent to Gherkin's Scenario Outline — one row per test case, columns for inputs and expected values.

Script Tables express multi-step flows. Script Tables let you write sequential procedural steps, making them the right choice for workflow testing where order and state matter.

FitNesse suits legacy and regulated systems. Its browser-based authoring, self-contained deployment, and long history in financial and insurance software make it the practical choice when stakeholders cannot use Git or an IDE.

What Is FitNesse?

FitNesse is an open-source acceptance testing framework and wiki server originally created by Robert C. Martin and Micah Martin. The core concept is radical: test specifications live inside a wiki, not in a code repository. Testers and business analysts write test tables in their browser; developers write "fixtures" — thin adapter classes — that connect the tables to the real application.

When you press the "Test" button on a wiki page, FitNesse executes the tables, compares outputs to expectations, and color-codes the wiki page: green for passing, red for failing, yellow for errors. No separate report to open, no CI dashboard to check — the wiki is the test report.

This model has made FitNesse popular in environments where:

  • Business stakeholders must own test content without touching code or files
  • The application under test is a legacy Java system with a thick integration layer
  • Regulatory requirements demand human-readable audit trails of acceptance tests

Installation

FitNesse ships as a single executable JAR with no external dependencies:

# Download the latest release
curl -O https://fitnesse.org/fitnesse-standalone.jar

# Start the wiki server on port 8080
java -jar fitnesse-standalone.jar -p 8080

Open http://localhost:8080 in your browser. The FitNesse wiki is ready. No database, no application server, no configuration file required.

For Maven-based Java projects, add the plugin to avoid maintaining a separate JAR:

<!-- pom.xml -->
<plugin>
    <groupId>uk.co.javahelp.fitnesse</groupId>
    <artifactId>fitnesse-launcher-maven-plugin</artifactId>
    <version>1.6.0</version>
    <configuration>
        <port>9090</port>
        <launches>
            <launch>
                <suite>FitNesseSuite</suite>
            </launch>
        </launches>
    </configuration>
</plugin>
mvn fitnesse:run   # runs all suites and exits with non-zero code on failure

SLiM vs FIT

FitNesse supports two test execution protocols:

FIT SLiM
Status Legacy Recommended
Protocol HTTP table parsing TCP socket (text protocol)
Error messages Cryptic Readable
Language support Java primarily Java, Python, C#, Ruby, C++
Table types ColumnFixture, RowFixture Decision, Query, Script, Subset, Ordered
Setup Complex classpath Simple, explicit

Use SLiM for any new project. To activate it on a wiki page, add:

!define TEST_SYSTEM {slim}

Or set it globally in FitNesseRoot/properties.xml.

Decision Tables

A Decision Table maps input columns to expected output columns. Each row is one test case. FitNesse calls the fixture once per row, sets input properties, invokes execute(), then reads output properties and compares them to the expected values.

Wiki markup:

|Discount Calculator|
|order total|customer tier|discount percent?|
|100.00     |standard     |0                |
|100.00     |silver       |5                |
|500.00     |gold         |15               |
|1000.00    |platinum     |20               |

The first row names the fixture class. Input columns map to setter methods; output columns (marked with ?) map to getter methods.

Java fixture:

package fixtures;

public class DiscountCalculator {
    private double orderTotal;
    private String customerTier;

    // SLiM calls these setters for input columns
    public void setOrderTotal(double orderTotal) {
        this.orderTotal = orderTotal;
    }

    public void setCustomerTier(String customerTier) {
        this.customerTier = customerTier;
    }

    // SLiM calls execute() between setting inputs and reading outputs
    public void execute() {
        // Nothing needed here for stateless calculation
    }

    // SLiM calls this getter for the "discount percent?" column
    public int discountPercent() {
        DiscountService service = new DiscountService();
        return service.calculate(orderTotal, customerTier);
    }
}

FitNesse colors each cell individually: green if the actual value matches expected, red if not.

Script Tables

Script Tables express sequential steps — actions and assertions in order. They are the right tool for workflow testing where each step depends on the previous one.

|script|Order Workflow|
|create customer|alice@example.com|
|add product to cart|Mechanical Keyboard|SKU-1001|
|apply coupon|SUMMER20|
|check out with card|4111111111111111|
|check|order status|confirmed|
|check|email sent to|alice@example.com|true|

Java fixture:

package fixtures;

public class OrderWorkflow {
    private OrderService orderService;
    private Customer currentCustomer;
    private Order currentOrder;

    public OrderWorkflow() {
        this.orderService = new OrderService(TestConfig.dataSource());
    }

    public void createCustomer(String email) {
        currentCustomer = new Customer(email);
        orderService.save(currentCustomer);
    }

    public void addProductToCart(String name, String sku) {
        orderService.addToCart(currentCustomer, new Product(name, sku));
    }

    public void applyCoupon(String code) {
        orderService.applyCoupon(currentCustomer, code);
    }

    public void checkOutWithCard(String cardNumber) {
        currentOrder = orderService.checkout(currentCustomer, new CreditCard(cardNumber));
    }

    public String orderStatus() {
        return currentOrder.getStatus();
    }

    public boolean emailSentTo(String email) {
        return TestMailServer.hasEmailFor(email);
    }
}

Script Table actions (create customer, add product to cart) call methods by name with spaces converted to camelCase. check rows call the named method and compare the return value to the expected value.

Query Tables

Query Tables test the contents of a collection — the result of a database query, an API list response, or any iterable data set.

|query|Order History|alice@example.com|
|order id  |status    |total  |
|ORD-001   |delivered |149.99 |
|ORD-002   |confirmed |89.99  |

Java fixture:

package fixtures;

import java.util.List;
import java.util.Map;

public class OrderHistory {
    private String email;

    public OrderHistory(String email) {
        this.email = email;
    }

    // SLiM calls query() and expects List<List<List<String>>>
    // Each inner list is a row: [["column-name", "value"], ...]
    public List<List<List<String>>> query() {
        List<Order> orders = OrderRepository.findByEmail(email);
        return orders.stream()
            .map(o -> List.of(
                List.of("order id", o.getId()),
                List.of("status",   o.getStatus()),
                List.of("total",    String.valueOf(o.getTotal()))
            ))
            .collect(Collectors.toList());
    }
}

FitNesse compares the returned rows to the table rows. Missing rows turn yellow ("surplus"), extra expected rows turn red ("missing").

Page Structure and Suites

FitNesse organises tests hierarchically. A page whose name ends in Suite automatically aggregates all child pages as a test suite. The typical structure:

FitNesseSuite/                ← top-level suite page
  SetUp                       ← runs before every test in the suite
  TearDown                    ← runs after every test in the suite
  OrderTests/                 ← sub-suite
    CheckoutTest              ← individual test page
    RefundTest                ← individual test page
  AuthTests/
    LoginTest
    PasswordResetTest

SetUp and TearDown pages at any level in the hierarchy apply to all tests below them. Use SuiteSetUp and SuiteTearDown for pages that run once per suite execution.

Build Tool Integration

For CI pipelines, FitNesse provides both Maven and Gradle integration that exits with a non-zero code on test failure.

Gradle:

// build.gradle
configurations {
    fitnesse
}

dependencies {
    fitnesse 'org.fitnesse:fitnesse:20231023'
}

task fitnesseTest(type: JavaExec) {
    classpath = configurations.fitnesse + sourceSets.main.runtimeClasspath
    mainClass = 'fitnesseMain.FitNesseMain'
    args = ['-p', '9090', '-c', 'FitNesseSuite?suite&format=text', '-b', 'fitnesse-results.txt']
    doLast {
        if (executionResult.get().exitValue != 0) {
            throw new GradleException("FitNesse tests failed")
        }
    }
}

When to Use FitNesse vs Cucumber

Criterion FitNesse Cucumber / SpecFlow
Test authoring tool Browser (wiki) Text editor / IDE
Specification format Wiki tables Gherkin (Given/When/Then)
Primary language Java Multi-language
Learning curve for BAs Low (just tables) Medium (Gherkin syntax)
Developer tooling Limited Excellent (IDE plugins)
Version control FitNesse wiki (Git optional) Always in Git
Ecosystem maturity Stable, smaller Large, very active
Best fit Legacy Java, regulated industries, BA-owned tests Greenfield, developer-friendly, CI-first

FitNesse is the right choice when business analysts need to write and own tests without any Git or IDE involvement, and the system under test is predominantly Java. Cucumber / SpecFlow are better when the development team leads BDD adoption, the test suite lives in Git alongside production code, and IDE integration matters.

Integration with HelpMeTest

FitNesse tests verify that your application's business logic produces the right outputs given controlled inputs. What they cannot verify is whether the deployed application works end-to-end in the real browser, with real network conditions, for real users.

HelpMeTest covers the production layer that FitNesse leaves open. You describe test scenarios in plain English — the same language FitNesse tables already use — and HelpMeTest runs them against your live URL on a schedule. No fixtures to write, no wiki server to maintain; HelpMeTest's AI interprets the steps and drives a real browser.

The practical combination: FitNesse tables own the acceptance criteria and decision-table coverage; HelpMeTest monitors the deployed application 24/7 and alerts you when something breaks in production that your FitNesse suite never caught.

Summary

FitNesse occupies a unique niche in the testing landscape: a self-contained, browser-based acceptance testing environment that requires no Git, no IDE, and no command-line knowledge to operate. Its table-driven format is immediately accessible to business analysts, and its long history in Java enterprise development means that integration with existing Java systems is well-understood. For new, collaborative, developer-led BDD projects, Cucumber or SpecFlow will feel more natural. For regulated domains, legacy Java systems, or teams where business stakeholders must own test authoring, FitNesse remains the most practical choice. Complement it with HelpMeTest to extend that acceptance coverage into production monitoring.

Read more

Start now free