Test Interfaces and Default Methods

This is a preview version that is not considered stable.
For the latest stable release, please see JUnit 6.0.3!

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.