FitNesse SLIM: Advanced Acceptance Testing with Decision Tables and Scenarios

FitNesse SLIM: Advanced Acceptance Testing with Decision Tables and Scenarios

FitNesse is older than most modern testing frameworks and carries that age visibly — a wiki-based test editor, table-driven specs, Java-centric tooling. But it solves a specific problem exceptionally well: getting non-technical business stakeholders to write and own acceptance tests, structured as business rules rather than test scripts. When that is the actual requirement, FitNesse SLIM is still the right tool.

SLIM vs FIT: Why It Matters

FitNesse started with the FIT (Framework for Integrated Testing) protocol. FIT required fixture classes to extend framework base classes, coupling test fixtures tightly to FitNesse internals. SLIM (Simple List Invocation Method) replaced FIT with a clean protocol: FitNesse communicates with test fixtures over a socket using simple list-based messages. Fixtures are plain Java (or Python, Ruby, .NET) classes — no inheritance required.

// FIT fixture (legacy) — extends ColumnFixture
public class PriceCalculatorFixture extends ColumnFixture {
    public String product;
    public int quantity;
    public double price() {
        return PriceCalculator.calculate(product, quantity);
    }
}

// SLIM fixture — plain class, no base class
public class PriceCalculatorFixture {
    private String product;
    private int quantity;

    public void setProduct(String product) { this.product = product; }
    public void setQuantity(int quantity) { this.quantity = quantity; }
    public double price() {
        return PriceCalculator.calculate(product, quantity);
    }
}

The SLIM class is testable in isolation, injectable with mocks, and compilable without FitNesse on the classpath. This is the right default for any new FitNesse work.

Decision Tables

Decision Tables are the workhouse of FitNesse SLIM. Each row is a test case; input columns drive the fixture, output columns assert results.

|decision: Price Calculator|
|product     |quantity|price?|
|Widget      |1       |9.99  |
|Widget      |10      |89.90 |
|Gadget      |1       |24.99 |
|Gadget      |5       |112.46|

The decision: prefix tells SLIM to use Decision Table mode. For each row:

  1. FitNesse calls setter methods for input columns (setProduct, setQuantity)
  2. FitNesse calls getter methods for output columns (price)
  3. The returned value is compared to the expected value in the cell

Column names map to methods by convention: product → setProduct(), price? → price() (the ? suffix marks it as an output column).

Handling null and error cases — use FitNesse's special cell values:

|decision: Account Validator|
|email              |password |valid?|error?     |
|user@example.com   |correct  |true  |           |
|user@example.com   |wrong    |false |            |
|notanemail         |anything |false |Invalid email|

The error? column maps to an error() method that returns the validation error message (or null/empty for valid input).

Query Tables

Query Tables validate the contents of a data set — a database query result, an API response list, a file's rows. The fixture returns a list, and FitNesse compares it against the expected table.

|query: Active Users|
|username  |role  |lastLogin  |
|alice     |admin |2024-01-15 |
|bob       |user  |2024-01-14 |
public class ActiveUsersFixture {
    public List<List<List<String>>> query() {
        List<User> users = userRepository.findActive();
        return users.stream()
            .map(u -> Arrays.asList(
                Arrays.asList("username", u.getUsername()),
                Arrays.asList("role", u.getRole()),
                Arrays.asList("lastLogin", u.getLastLogin().toString())
            ))
            .collect(Collectors.toList());
    }
}

FitNesse matches rows by content, not position — it reports surplus rows (in actual but not expected) and missing rows (in expected but not actual). This makes Query Tables robust to ordering changes in the underlying data source.

Script Tables

Script Tables express sequential steps — the kind of interactions that do not fit into a single decision or query model. They are closest to what you would write in a standard automated test.

|script: Checkout Flow|
|start browser                           |
|navigate to           |/products        |
|click                 |Add to Cart      |
|click                 |View Cart        |
|check|cart item count |1               |
|click                 |Checkout         |
|enter                 |user@example.com |in field|email|
|enter                 |secret123        |in field|password|
|click                 |Place Order      |
|check|order confirmation visible|true  |

Script Table methods map from the table syntax:

  • start browser → startBrowser()
  • navigate to /products → navigateTo(String url)
  • click Add to Cart → click(String button)
  • check | cart item count | 1 → calls cartItemCount() and asserts it equals 1
  • enter email in field email → enter(String value, String field)
public class CheckoutFlowFixture {
    private WebDriver driver;

    public void startBrowser() {
        driver = new ChromeDriver();
    }

    public void navigateTo(String path) {
        driver.get(BASE_URL + path);
    }

    public void click(String buttonText) {
        driver.findElement(By.linkText(buttonText)).click();
    }

    public int cartItemCount() {
        return driver.findElements(By.cssSelector(".cart-item")).size();
    }

    public void enter(String value, String field) {
        driver.findElement(By.name(field)).sendKeys(value);
    }

    public boolean orderConfirmationVisible() {
        return driver.findElements(By.cssSelector(".order-confirmation")).size() > 0;
    }
}

Scenario Libraries

Scenario Libraries define reusable step sequences that any test page in the same suite can reference. Define a library page named ScenarioLibrary (FitNesse convention):

|scenario|login as admin|
|script: Auth Steps   |
|navigate to |/login  |
|enter |admin@example.com |in field|email|
|enter |adminpass |in field|password|
|click |Sign In   |
|check |current page|/dashboard|

Reference it in any test page:

|script|
|login as admin|
|navigate to |/admin/users|
|check|user count|42|

Scenario Libraries are the FitNesse equivalent of Cucumber step reuse — the logic lives once, the intent is readable everywhere.

SuiteSetUp and SuiteTearDown

FitNesse's lifecycle pages are special wiki pages recognized by name:

  • SuiteSetUp — runs before the first test in a suite. Wire up database connections, start embedded servers, seed reference data.
  • SuiteTearDown — runs after the last test. Clean up resources, stop servers.
  • SetUp — runs before each test page in the suite.
  • TearDown — runs after each test page.

These pages use the same table syntax as test pages. A typical SuiteSetUp:

|script: Suite Initializer|
|start database with schema|test_schema|
|seed reference data from  |fixtures/seed.sql|
|start application server on port|8080|
public class SuiteInitializerFixture {
    public void startDatabaseWithSchema(String schema) {
        TestDatabase.start(schema);
    }

    public void seedReferenceDataFrom(String sqlFile) {
        TestDatabase.execute(new File(sqlFile));
    }

    public void startApplicationServerOnPort(int port) {
        TestServer.start(port);
    }
}

Importing External JAR Fixtures

FitNesse loads fixtures from the classpath. For fixtures in external JARs (a common pattern for separating test infrastructure from test specs):

!path /path/to/fixture-library.jar
!path /path/to/dependencies/*.jar

Or use FitNesse's library path variables for portability across environments:

!define FIXTURE_JAR {${FIXTURE_HOME}/fixture-library.jar}
!path ${FIXTURE_JAR}

Set FIXTURE_HOME as a JVM property or in the plugins.properties file. This makes the suite work across developer machines and CI without hardcoded paths.

For Maven projects, the standard approach is to run FitNesse as part of the integration-test phase after the fixture project is built:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <executions>
    <execution>
      <phase>integration-test</phase>
      <goals><goal>java</goal></goals>
      <configuration>
        <mainClass>fitnesseMain.FitNesseMain</mainClass>
        <arguments>
          <argument>-p</argument><argument>9090</argument>
          <argument>-c</argument><argument>MySuite?suite&amp;format=junit</argument>
        </arguments>
      </configuration>
    </execution>
  </executions>
</plugin>

Organizing Large FitNesse Test Suites

A few structural decisions that make large suites manageable:

Suite hierarchy maps to domain — organize by business area, not test type. MySuite.Checkout, MySuite.Inventory, MySuite.Auth — not MySuite.HappyPath, MySuite.ErrorCases.

One concept per page — Decision Tables work best with one fixture per page. Mixing multiple tables with multiple fixtures on one page makes failures ambiguous.

Tags for selective execution — FitNesse supports !meta suite-tags: smoke, regression on test pages. Run tagged subsets:

http://localhost:9090/MySuite?suite&runTestsMatchingTags=smoke

Fixture naming convention — suffix all fixture classes with Fixture. PriceCalculatorFixture, UserQueryFixture. This makes it trivially clear what is a fixture vs application code during code reviews.

Avoid business logic in fixtures — fixtures should translate between FitNesse tables and application APIs, nothing more. When a fixture does string manipulation, date formatting, or conditional logic, that logic belongs in the application layer with its own unit tests.

Bridging to Continuous Monitoring

FitNesse suites run in CI against test or staging environments. Production behavior — under real load, with real third-party services, after configuration changes — is a different verification problem.

HelpMeTest fills that gap: define critical user journeys in plain English, run them against production on a schedule. When a FitNesse Decision Table validates a pricing rule and HelpMeTest monitors the pricing page in production, the coverage is end-to-end. FitNesse catches regressions at the logic level before they ship; HelpMeTest catches degradations in production after they ship.

Read more

Start now free