Skip to content

nchaugen/tabletest

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TableTest

TableTest extends JUnit for data-driven testing using a concise 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.0-6.0.0 (except 5.13.0).

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 this priority:

  1. Factory method (in test class or @FactorySources)
  2. Built-in conversion (primitives, dates, enums, etc.)

Factory Methods

Factory methods are public static methods in a public class that accept one parameter and return the target type:

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

TableTest will look for a factory method present in either the test class, including inherited methods, or in one of the classes listed by a @FactorySources annotation. The first factory method found will be used. If required, TableTest will first convert the cell value to match the factory method parameter type, before invoking the factory method to convert it to the test method parameter type.

There is no specific naming pattern for factory methods, any method fulfilling the requirements above will be considered. Only one factory method per target type is possible per class.

Built-In Conversion

TableTest falls back to JUnit's built-in type converters if no factory method is found.

@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.

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());
}

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.

TableTest is compatible with JUnit versions 5.11.0 to 6.0.0 (except 5.13.0).

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.

Please note that TableTest requires Java version 21 or above.

Maven (pom.xml)

<dependencies>
    <dependency>
        <groupId>io.github.nchaugen</groupId>
        <artifactId>tabletest-junit</artifactId>
        <version>0.5.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>6.0.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Gradle with Kotlin DSL (build.gradle.kts)

dependencies { 
    testImplementation("io.github.nchaugen:tabletest-junit:0.5.2")
    testImplementation("org.junit.jupiter:junit-jupiter:6.0.0")
    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.

Additionally, tabletest-junit distributions prior to version 0.5.1 included the following modules from JUnit 5 which are released under Eclipse Public License 2.0:

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

Starting from version 0.5.1, these modules are 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.

About

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

Resources

License

Stars

Watchers

Forks

Packages

No packages published