Test Interfaces and Default Methods

JUnit Jupiter allows @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate, @BeforeEach, and @AfterEach to be declared on interface default methods. @BeforeAll and @AfterAll can either be declared on static methods in a test interface or on interface default methods if the test interface or test class is annotated with @TestInstance(Lifecycle.PER_CLASS) (see Test Instance Lifecycle). Here are some examples.

  • Java
  • Kotlin
@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {

    Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());

    @BeforeAll
    default void beforeAllTests() {
        logger.info("Before all tests");
    }

    @AfterAll
    default void afterAllTests() {
        logger.info("After all tests");
    }

    @BeforeEach
    default void beforeEachTest(TestInfo testInfo) {
        logger.info(() -> "About to execute [%s]".formatted(
            testInfo.getDisplayName()));
    }

    @AfterEach
    default void afterEachTest(TestInfo testInfo) {
        logger.info(() -> "Finished executing [%s]".formatted(
            testInfo.getDisplayName()));
    }

}
@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {
    @BeforeAll
    fun beforeAllTests() {
        logger.info("Before all tests")
    }

    @AfterAll
    fun afterAllTests() {
        logger.info("After all tests")
    }

    @BeforeEach
    fun beforeEachTest(testInfo: TestInfo) {
        logger.info { "About to execute [${testInfo.displayName}]" }
    }

    @AfterEach
    fun afterEachTest(testInfo: TestInfo) {
        logger.info { "Finished executing [${testInfo.displayName}]" }
    }

    companion object {
        private val logger: Logger = Logger.getLogger(TestLifecycleLogger::class.java.name)
    }
}
  • Java
  • Kotlin
interface TestInterfaceDynamicTestsDemo {

    @TestFactory
    default Stream<DynamicTest> dynamicTestsForPalindromes() {
        return Stream.of("racecar", "radar", "mom", "dad")
            .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
    }

}
interface TestInterfaceDynamicTestsDemo {
    @TestFactory
    fun dynamicTestsForPalindromes(): Sequence<DynamicTest> =
        sequenceOf("racecar", "radar", "mom", "dad")
            .map { text -> dynamicTest(text) { assertTrue(isPalindrome(text)) } }
}

@ExtendWith and @Tag can be declared on a test interface so that classes that implement the interface automatically inherit its tags and extensions. See Before and After Test Execution Callbacks for the source code of the TimingExtension.

  • Java
  • Kotlin
@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}
@Tag("timed")
@ExtendWith(TimingExtension::class)
interface TimeExecutionLogger

In your test class you can then implement these test interfaces to have them applied.

  • Java
  • Kotlin
class TestInterfaceDemo implements TestLifecycleLogger,
        TimeExecutionLogger, TestInterfaceDynamicTestsDemo {

    @Test
    void isEqualValue() {
        assertEquals(1, "a".length(), "is always equal");
    }

}
class TestInterfaceDemo :
    TestLifecycleLogger,
    TimeExecutionLogger,
    TestInterfaceDynamicTestsDemo {
    @Test
    fun isEqualValue() {
        assertEquals(1, "a".length, "is always equal")
    }
}

Running the TestInterfaceDemo results in output similar to the following:

INFO  example.TestLifecycleLogger - Before all tests
INFO  example.TestLifecycleLogger - About to execute [dynamicTestsForPalindromes()]
INFO  example.TimingExtension - Method [dynamicTestsForPalindromes] took 19 ms.
INFO  example.TestLifecycleLogger - Finished executing [dynamicTestsForPalindromes()]
INFO  example.TestLifecycleLogger - About to execute [isEqualValue()]
INFO  example.TimingExtension - Method [isEqualValue] took 1 ms.
INFO  example.TestLifecycleLogger - Finished executing [isEqualValue()]
INFO  example.TestLifecycleLogger - After all tests

Another possible application of this feature is to write tests for interface contracts. For example, you can write tests for how implementations of Object.equals or Comparable.compareTo should behave as follows.

  • Java
  • Kotlin
public interface Testable<T> {

    T createValue();

}
interface Testable<T> {
    fun createValue(): T
}
  • Java
  • Kotlin
public interface EqualsContract<T> extends Testable<T> {

    T createNotEqualValue();

    @Test
    default void valueEqualsItself() {
        T value = createValue();
        assertEquals(value, value);
    }

    @Test
    default void valueDoesNotEqualNull() {
        T value = createValue();
        assertNotEquals(null, value);
    }

    @Test
    default void valueDoesNotEqualDifferentValue() {
        T value = createValue();
        T differentValue = createNotEqualValue();
        assertNotEquals(value, differentValue);
        assertNotEquals(differentValue, value);
    }

}
interface EqualsContract<T> : Testable<T> {
    fun createNotEqualValue(): T

    @Test
    fun valueEqualsItself() {
        val value = createValue()
        assertEquals(value, value)
    }

    @Test
    fun valueDoesNotEqualNull() {
        val value = createValue()
        assertNotEquals(null, value)
    }

    @Test
    fun valueDoesNotEqualDifferentValue() {
        val value = createValue()
        val differentValue = createNotEqualValue()
        assertNotEquals(value, differentValue)
        assertNotEquals(differentValue, value)
    }
}
  • Java
  • Kotlin
public interface ComparableContract<T extends Comparable<T>> extends Testable<T> {

    T createSmallerValue();

    @Test
    default void returnsZeroWhenComparedToItself() {
        T value = createValue();
        assertEquals(0, value.compareTo(value));
    }

    @Test
    default void returnsPositiveNumberWhenComparedToSmallerValue() {
        T value = createValue();
        T smallerValue = createSmallerValue();
        assertTrue(value.compareTo(smallerValue) > 0);
    }

    @Test
    default void returnsNegativeNumberWhenComparedToLargerValue() {
        T value = createValue();
        T smallerValue = createSmallerValue();
        assertTrue(smallerValue.compareTo(value) < 0);
    }

}
interface ComparableContract<T : Comparable<T>> : Testable<T> {
    fun createSmallerValue(): T

    @Test
    fun returnsZeroWhenComparedToItself() {
        val value = createValue()
        assertEquals(0, value.compareTo(value))
    }

    @Test
    fun returnsPositiveNumberWhenComparedToSmallerValue() {
        val value = createValue()
        val smallerValue = createSmallerValue()
        assertTrue(value > smallerValue)
    }

    @Test
    fun returnsNegativeNumberWhenComparedToLargerValue() {
        val value = createValue()
        val smallerValue = createSmallerValue()
        assertTrue(smallerValue < value)
    }
}

In your test class you can then implement both contract interfaces thereby inheriting the corresponding tests. Of course you’ll have to implement the abstract methods.

  • Java
  • Kotlin
class StringTests implements ComparableContract<String>, EqualsContract<String> {

    @Override
    public String createValue() {
        return "banana";
    }

    @Override
    public String createSmallerValue() {
        return "apple"; // 'a' < 'b' in "banana"
    }

    @Override
    public String createNotEqualValue() {
        return "cherry";
    }

}
class StringTests :
    ComparableContract<String>,
    EqualsContract<String> {
    override fun createValue() = "banana"

    override fun createSmallerValue() = "apple" // 'a' < 'b' in "banana"

    override fun createNotEqualValue() = "cherry"
}
The above tests are merely meant as examples and therefore not complete.