Skip to content

Extension to JUnit for data-driven testing with the readable TableTest format

License

Notifications You must be signed in to change notification settings

nchaugen/tabletest

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

191 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Important

TableTest has new Maven coordinates: org.tabletest:tabletest-junit:1.0.0

Please update your dependencies to keep receiving updates.

TableTest

TableTest extends JUnit for data-driven testing using a concise and readable table format. Express system behaviour through multiple examples, reducing test code while improving readability and maintainability.

@TableTest("""
    Scenario                     | Example Years      | Is Leap Year?
    Not divisible by 4           | {1, 2001, 30001}   | No
    Divisible by 4               | {4, 2004, 30008}   | Yes
    Divisible by 100, not by 400 | {100, 2100, 30300} | No
    Divisible by 400             | {400, 2000, 30000} | Yes
    Year 0                       | 0                  | Yes
    Negative input               | -1                 | No
    """)
void testLeapYears(Year year, boolean isLeapYear) {
    assertEquals(isLeapYear, year.isLeap());
}

public static boolean parseBoolean(String input) {
    return input.equalsIgnoreCase("yes");
}

Benefits:

  • Readable: Clear input/output relationships in structured tables
  • Maintainable: Add test cases by adding table rows
  • Concise: Eliminates repetitive test code
  • Self-documenting: Tables serve as living documentation
  • Collaborative: Non-technical stakeholders can understand and contribute

Requirements: Java 21+, JUnit 5.11+

IDE Support: TableTest plugin for IntelliJ provides auto-formatting, syntax highlighting, and shortcuts for working with tables.

Latest Updates: See the changelog for details on recent releases and changes.

User Guide: See the user guide for more details on how to use TableTest.

Blog Posts: See this blog post for a short introduction to table-driven testing and TableTest.

Table of Contents

Usage

Annotate test methods with @TableTest and provide table data as a multi-line string or external file.

Tables use pipes (|) to separate columns. The first row contains headers, the following rows contain test data. Each data row invokes the test method with cell values as arguments.

@TableTest("""
    Scenario                              | Year | Is leap year?
    Years not divisible by 4              | 2001 | false
    Years divisible by 4                  | 2004 | true
    Years divisible by 100 but not by 400 | 2100 | false
    Years divisible by 400                | 2000 | true
    """)
public void leapYearCalculation(Year year, boolean expectedResult) {
    assertEquals(expectedResult, year.isLeap(), "Year " + year);
}

Key points:

  • One parameter per data column (scenario column excluded)
  • Parameters map by position, not name
  • Values automatically convert to parameter types
  • Test methods must be non-private, non-static, void return

Technically @TableTest is a JUnit @ParameterizedTest with a custom-format argument source.

Value Formats

The TableTest format supports four types of values:

  • Single values specified with or without quotes (abc, "a|b", ' ')
  • Lists of elements enclosed in brackets ([1, 2, 3])
  • Sets of elements enclosed in curly braces ({a, b, c})
  • Maps of key:value pairs enclosed in brackets ([a: 1, b: 2]).

Lists, sets, and maps can be nested ([a: [1, 2, 3], b: [4, 5, 6]]).

@TableTest("""
    Single value           | List        | Set               | Map
    Hello, world!          | [1, 2, 3]   | {1, 2, 3}         | [a: 1, b: 2, c: 3]
    'cat file.txt | wc -l' | [a, '|', b] | {[], [1], [1,2 ]} | [empty: {}, full: {1, 2, 3}]
    ""                     | []          | {}                | [:]
    """)
void testValues(String single, List<?> list, Set<?> set, Map<String, ?> map) {
    //...
}

Value Conversion

TableTest converts table values to method parameter types using the following mechanisms (in order of priority):

  1. Custom type converter method (in test class or in class referenced by @TypeConverterSources)
  2. Built-in conversion (primitives, dates, enums, etc.)

Custom Type Converter Methods

Custom type converter methods are public static methods in a public class annotated with @TypeConverter. The methods must accept exactly one parameter and return a value in the target type:

@TypeConverter
public static LocalDate convertToDate(String input) {
    return switch (input) {
        case "today" -> LocalDate.now();
        case "tomorrow" -> LocalDate.now().plusDays(1);
        default -> LocalDate.parse(input);
    };
}

TableTest will search for a matching custom converter method in the test class, including inherited methods, and in any classes listed by a @TypeConverterSources annotation on the test class. The first custom converter with a return type matching the target type will be used.

TableTest is able to chain converters to transform a value to the target test method parameter type. Please see the user guide for more information about this powerful feature.

There is no specific naming pattern for custom converter methods, any method tagged with the @TypeConverter annotation fulfilling the requirements of a type converter will be considered. Only one type converter method per target type is possible per class.

Built-In Conversion

TableTest falls back to JUnit's built-in type converters if no suitable custom converter method is found. This covers most standard Java types,

@TableTest("""
    Number | Text | Date       | Class
    1      | abc  | 2025-01-20 | java.lang.Integer
    """)
void singleValues(short number, String text, LocalDate date, Class<?> type) {
    // test implementation
}

Parameterized Types

TableTest will convert elements in compound values like List, Set, and Map to match parameterized types. Nested values are also traversed and converted. Map keys remain String type and are not converted. Both custom and build-in converters will be considered for conversion.

In the example below, the list of grades inside the map is converted to List<Integer>:

@TableTest("""
    Grades                                       | Highest Grade?
    [Alice: [95, 87, 92], Bob: [78, 85, 90]]     | 95
    [Charlie: [98, 89, 91], David: [45, 60, 70]] | 98
    """)
void testParameterizedTypes(Map<String, List<Integer>> grades, int expectedHighestGrade) {
    // test implementation
}

Null Values

Blank cells will translate to null for all parameter types except primitives. For primitives, it will cause an exception as they cannot represent a null value.

@TableTest("""
    String | Integer | List | Map | Set
           |         |      |     |
    """)
void blankConvertsToNull(String string, Integer integer, List<?> list, Map<String, ?> map, Set<?> set) {
    assertNull(string);
    assertNull(integer);
    assertNull(list);
    assertNull(map);
    assertNull(set);
}

Additional Features

TableTest contains a number of other useful features for expressing examples in a table format.

Scenario Names

Add descriptive names to test rows by providing a scenario name in the first column:

@TableTest("""
    Scenario     | Input | Output
    Basic case   | 1     | one
    Edge case    | 0     | zero
    """)
void test(int input, String output) {
    // test implementation
}

Scenario names make the tables better documentation and will be used as test display names. This makes the test failures more clear and debugging easier.

Optionally scenario names can be accessed in test methods by declaring it as a test method parameter tagged with annotation @Scenario.

Value Sets

TableTest allows using a set in a single-value column to express that any of the listed values give the same result. This is a powerful feature that can be used to contract multiple rows that have identical expectations.

TableTest will create multiple test invocations for a row with a value set, one for each value in the set. The test method will be invoked 12 times, three times for each row, once for each value in the Example years set.

@TableTest("""
    Scenario                              | Example years      | Is leap year?
    Years not divisible by 4              | {2001, 2002, 2003} | false
    Years divisible by 4                  | {2004, 2008, 2012} | true
    Years divisible by 100 but not by 400 | {2100, 2200, 2300} | false
    Years divisible by 400                | {2000, 2400, 2800} | true
    """)
public void testLeapYear(Year year, boolean expectedResult) {
    assertEquals(expectedResult, year.isLeap(), "Year " + year);
}

Scenario names will be augmented to include the value from the set being used for the current test invocation. This makes it easier to see which values caused problems in case of test failures.

Value sets can be used multiple times in the same row. TableTest will then perform a cartesian product, generating test invocations for all possible combinations of values. Use this judiciously, as the number of test cases grows multiplicatively with each additional set, as does the test execution time.

Comments and Blank Lines

Lines starting with // (ignoring leading whitespace) are treated as comments and ignored. Comments allow adding explanations or temporarily disabling data rows.

Blank lines are also ignored and can be used to visually group related rows.

@TableTest("""
    String         | Length?
    
    Hello world    | 11
    
    // The next row is currently disabled
    // "World, hello" | 12
    
    // Special characters must be quoted
    '|'            | 1
    '[:]'          | 3
    """)
void testComment(String string, int expectedLength) {
    assertEquals(expectedLength, string.length());
}

Table in External File

Tables can be loaded from external files using the resource attribute. The file must be located as a resource relative to the test class. Typically, it is stored in the test resources directory or one of its subdirectories.

By default, the file is assumed to use UTF-8 encoding. If your file uses a different encoding, specify it with the encoding attribute.

@TableTest(resource = "/external.table")
void testExternalTable(int a, int b, int sum) {
    assertEquals(sum, a + b);
}

@TableTest(resource = "/custom-encoding.table", encoding = "ISO-8859-1")
void testExternalTableWithCustomEncoding(String string, int expectedLength) {
    assertEquals(expectedLength, string.length());
}

Publishing TablesTest results

Functionality for publishing TableTest results to AsciiDoc and Markdown format is available as extension TableTest-Reporter.

Installation

TableTest is available from Maven Central Repository. Projects using Maven or Gradle build files can simply add TableTest as a test scope dependency alongside JUnit.

Java and JUnit Compatibility

TableTest requires Java version 21 or above and is compatible with JUnit 5.11 and above.

Frameworks such as Quarkus and SpringBoot packages their own version of JUnit. TableTest is compatible with Quarkus version 3.21.2 and above and SpringBoot version 3.4.0 and above. Please see the compatibility tests for examples of how to use TableTest with these frameworks.

Compatibility Notes

Note that TableTest versions 0.5.4 - 0.5.7 needed JUnit 5.14 and above. JUnit 5.13.0 introduced breaking changes that broke compatibility with TableTest. This was fixed in JUnit 5.13.1.

Maven (pom.xml)

<dependencies>
    <dependency>
        <groupId>org.tabletest</groupId>
        <artifactId>tabletest-junit</artifactId>
        <version>1.0.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>6.0.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Gradle with Kotlin DSL (build.gradle.kts)

dependencies {
    testImplementation("org.tabletest:tabletest-junit:1.0.0")
    testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

IDE Support

The TableTest plugin for IntelliJ enhances your development experience when working with TableTest format tables. The plugin provides:

  • Code assistance for table formatting
  • Syntax highlighting for table content
  • Visual feedback for invalid table syntax

Installing the plugin streamlines the creation and maintenance of data-driven tests, making it easier to work with both inline and external table files.

License

TableTest is licensed under the liberal and business-friendly Apache Licence, Version 2.0 and is freely available on GitHub.

TableTest distributions prior to version 0.5.1 included the following modules from JUnit 5 released under Eclipse Public License 2.0:

  • org.junit.jupiter:junit-jupiter-params
  • org.junit.platform:junit-platform-commons

From version 0.5.1 onwards, these modules were no longer included in the tabletest-junit distribution.

TableTest binaries are published to the repositories of Maven Central. The artefacts signatures can be validated against this PGP public key.