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