Skip to content

mikemybytes/junit5-formatted-source

Repository files navigation

JUnit 5 FormattedSource

This library extends JUnit 5 with a new way of writing parameterized tests. It allows defining test case arguments in a human-readable way, following a user-defined format. Additionally, it automatically takes care of the test case names, so the input definition is also what will be presented in the test execution output.

class CalculatorTest {
    
    private final Calculator calculator = new Calculator();

    @FormattedSourceTest(format = "{0} + {1} = {2}", lines = {
            "1 + 2 = 3",
            "3 + 4 = 7"
    })
    void calculatesSum(int a, int b, int expectedSum) {
        Assertions.assertEquals(expectedSum, calculator.sum(a, b));
    }
    
}

Output:

calculatesSum(int, int, int) ✔
├─ 1 + 2 = 3 ✔
└─ 3 + 4 = 7 ✔

Of course, JUnit 5 FormattedSource can do even more!

Installing

Requirements

  • Java 11+
  • JUnit 5.8.0+

Note: The library does not introduce any dependencies other than the JUnit 5.

Maven

    <dependency>
        <groupId>com.mikemybytes</groupId>
        <artifactId>junit5-formatted-source</artifactId>
        <version>1.0.1</version>
        <scope>test</scope>
    </dependency>

Gradle

    testImplementation "com.mikemybytes:junit5-formatted-source:1.0.1"

Java Platform Module System (JPMS)

Java module name: com.mikemybytes.junit5.formatted (descriptor, example usage)

User Guide

Details and usage examples can be found in the project's User Guide.

Yet another argument source?

The project has been inspired by the built-in @CsvSource annotation, which allows writing not only data table tests but also specification-like test case definitions:

class CsvSourceSpecificationTest {
    
    @ParameterizedTest(name = "{0} maps to {1}")
    @CsvSource(delimiterString = "maps to", textBlock = """
        'foo' maps to 'bar'
        'junit' maps to 'jupiter'
        """)
    void mapsOneValueToAnother(String input, String expectedValue) {
        // ...
    }
    
}

Yet, the @CsvSource limits the user to only one delimiterString, which effectively means supporting only two arguments at a time. Additionally, selected delimiter must be repeated within the @ParameterizedTest's name parameter in order to appear in the test execution output.

Using @FormattedSource allows to forget about these limitations and write less code:

class FormattedSourceSpecificationTest {
    
    @FormattedSourceTest(format = "{0} maps to {1} using rule {2}", textBlock = """
        'foo' maps to 'bar' using rule 486
        'junit' maps to 'jupiter' using rule 44
        """)
    void mapsOneValueToAnother(String input, String expectedValue, int expectedRuleId) {
        // ...
    }
    
}

Test case names are automatically generated based on provided specification:

mapsOneValueToAnother(String, String, int) ✔
├─ 'foo' maps to 'bar' using rule 486 ✔
└─ 'junit' maps to 'jupiter' using rule 44 ✔

@FormattedSourceTest vs @FormattedSource

The library comes with two annotations. @FormattedSource is just a standard JUnit 5 argument source that has to be combined with @ParameterizedTest. It also does not influence generated test case names automatically:

class DurationEncodingTest {
    
    @ParameterizedTest(name = "encodes {0} seconds as {1}")
    @FormattedSource(format = "encodes {0} seconds as {1}", lines = {
            "encodes 15 seconds as 'PT15S'",
            "encodes 180 seconds as 'PT3M'",
            "encodes 172800 seconds as 'PT48H'"
    })
    void encodesDurationAsIso8601(long seconds, String expected) {
        // ...
    }
    
}

The equivalent @FormattedSourceTest simply results in a less verbose code:

class DurationEncodingShorterTest {
    
    // @ParameterizedTest already included (with test case name!)
    @FormattedSourceTest(format = "encodes {0} seconds as {1}", lines = {
            "encodes 15 seconds as 'PT15S'",
            "encodes 180 seconds as 'PT3M'",
            "encodes 172800 seconds as 'PT48H'"
    })
    void encodesDurationAsIso8601(long seconds, String expected) {
        // ...
    }
    
}

Building from source

The project comes with Maven Wrapper, so it can be built even without Maven installed locally. There's no need to pass any additional properties.

The minimum Java version required to build the project is Java 17. Produced artifacts will be binary-compatible with Java 11+.

Build

./mvnw clean verify

Build & install

./mvnw clean install

License

The project is distributed under the MIT license.