Skip to content
🎉 TableTest 1.2.0 is out — array parameter support and Java 8+. Read the changelog.
Why TableTest?

Why TableTest?

A lot of software is about evaluating inputs against rules: validating fields, classifying data, triggering actions when thresholds are crossed. This is the kind of logic that benefits most from thorough, readable tests.

Consider leap year detection — a small function with four distinct rules. Most developers would write it like this:

@Test
void yearNotDivisibleBy4_isNotLeap() {
    assertFalse(Year.isLeap(2001));
}

@Test
void yearDivisibleBy4_isLeap() {
    assertTrue(Year.isLeap(2004));
}

@Test
void yearDivisibleBy100Not400_isNotLeap() {
    assertFalse(Year.isLeap(2100));
}

@Test
void yearDivisibleBy400_isLeap() {
    assertTrue(Year.isLeap(2000));
}

Four tests, four methods, four assertions. Each adds a new scenario by duplicating the structure of the others. The coverage is reasonable, but the signal-to-noise ratio is low. Reading these methods, you have to hold each assertion in mind and work out the pattern yourself.

The Table-Driven Approach

Table-driven testing separates test logic from test data. A single parameterised test method runs once per row in a table:

@TableTest("""
    Scenario                        | Year | Is Leap Year?
    Not divisible by 4              | 2001 | false
    Divisible by 4                  | 2004 | true
    Divisible by 100 but not by 400 | 2100 | false
    Divisible by 400                | 2000 | true
    """)
void leapYear(int year, boolean isLeapYear) {
    assertEquals(isLeapYear, Year.isLeap(year));
}

The Scenario column gives each row a name. It doesn’t require a matching parameter — TableTest recognises the extra column and uses it as a test display name. The remaining two columns map to year and isLeapYear by position.

TableTest converts the string values automatically. "2001" becomes int 2001; "false" becomes boolean false. No boilerplate conversion code needed.

Growing Coverage by Adding Rows

The real payoff comes when you want more coverage. With individual test methods, each new scenario means a new method. With a table, it means a new row:

@TableTest("""
    Scenario                        | Year  | Is Leap Year?
    Not divisible by 4              | 2001  | false
    Divisible by 4                  | 2004  | true
    Divisible by 100 but not by 400 | 2100  | false
    Divisible by 400                | 2000  | true
    Year 0 (ISO proleptic calendar) | 0     | true
    Far future leap                 | 2800  | true
    Very far future leap            | 30000 | true
    Very far future not leap        | 30100 | false
    Negative input                  | -1    | false
    """)
void leapYear(int year, boolean isLeapYear) {
    assertEquals(isLeapYear, Year.isLeap(year));
}

The test method stays the same. Nine scenarios, one method. A coverage gap you spotted while reviewing is just another row.

Grouping Values with Sets

TableTest supports value sets: multiple inputs in a single cell, wrapped in curly braces. When a cell contains {4, 2004, 30008}, TableTest runs the test once for each value. The table stays compact even as coverage grows:

@TableTest("""
    Scenario                        | Year               | Is Leap Year?
    Not divisible by 4              | {1, 2001, 30001}   | false
    Divisible by 4                  | {4, 2004, 30008}   | true
    Divisible by 100 but not by 400 | {100, 2100, 30300} | false
    Divisible by 400                | {400, 2000, 30000} | true
    Year 0 (ISO proleptic calendar) | 0                  | true
    Negative input                  | -1                 | false
    """)
void leapYear(int year, boolean isLeapYear) {
    assertEquals(isLeapYear, Year.isLeap(year));
}

Six rows, but the groupings make the rules visible at a glance. “Divisible by 4” covers 4, 2004, and 30008 — past, present, far future. The table communicates not just what’s tested, but why those values belong together.

Tables as Documentation

These tables can become useful descriptions of how the business and, consequently, the software work. The leap year table mirrors the logical rules in a way that a developer and a business expert could review together. When the rules change, the table changes — and the tests change with it.

This is the core premise of table-driven testing: test data is explicit, visible, and separate from test logic. Adding a scenario is a matter of adding a row. Coverage gaps are visible in the whitespace. And the table itself becomes a specification.

Getting Started

TableTest is a JUnit extension for Java and Kotlin. See the installation guide and your first test to get up and running quickly.

For a more complete example showing custom types, type conversion, and relative timestamps, see the realistic example in the documentation.