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

  1. @TestInstance(Lifecycle.PER_CLASS)
  2. interface TestLifecycleLogger {
  3. static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());
  4. @BeforeAll
  5. default void beforeAllTests() {
  6. logger.info("Before all tests");
  7. }
  8. @AfterAll
  9. default void afterAllTests() {
  10. logger.info("After all tests");
  11. }
  12. @BeforeEach
  13. default void beforeEachTest(TestInfo testInfo) {
  14. logger.info(() -> String.format("About to execute [%s]",
  15. testInfo.getDisplayName()));
  16. }
  17. @AfterEach
  18. default void afterEachTest(TestInfo testInfo) {
  19. logger.info(() -> String.format("Finished executing [%s]",
  20. testInfo.getDisplayName()));
  21. }
  22. }
  1. interface TestInterfaceDynamicTestsDemo {
  2. @TestFactory
  3. default Stream<DynamicTest> dynamicTestsForPalindromes() {
  4. return Stream.of("racecar", "radar", "mom", "dad")
  5. .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
  6. }
  7. }

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

  1. @Tag("timed")
  2. @ExtendWith(TimingExtension.class)
  3. interface TimeExecutionLogger {
  4. }

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

  1. class TestInterfaceDemo implements TestLifecycleLogger,
  2. TimeExecutionLogger, TestInterfaceDynamicTestsDemo {
  3. @Test
  4. void isEqualValue() {
  5. assertEquals(1, "a".length(), "is always equal");
  6. }
  7. }

Running the TestInterfaceDemo results in output similar to the following:

  1. INFO example.TestLifecycleLogger - Before all tests
  2. INFO example.TestLifecycleLogger - About to execute [dynamicTestsForPalindromes()]
  3. INFO example.TimingExtension - Method [dynamicTestsForPalindromes] took 19 ms.
  4. INFO example.TestLifecycleLogger - Finished executing [dynamicTestsForPalindromes()]
  5. INFO example.TestLifecycleLogger - About to execute [isEqualValue()]
  6. INFO example.TimingExtension - Method [isEqualValue] took 1 ms.
  7. INFO example.TestLifecycleLogger - Finished executing [isEqualValue()]
  8. 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.

  1. public interface Testable<T> {
  2. T createValue();
  3. }
  1. public interface EqualsContract<T> extends Testable<T> {
  2. T createNotEqualValue();
  3. @Test
  4. default void valueEqualsItself() {
  5. T value = createValue();
  6. assertEquals(value, value);
  7. }
  8. @Test
  9. default void valueDoesNotEqualNull() {
  10. T value = createValue();
  11. assertFalse(value.equals(null));
  12. }
  13. @Test
  14. default void valueDoesNotEqualDifferentValue() {
  15. T value = createValue();
  16. T differentValue = createNotEqualValue();
  17. assertNotEquals(value, differentValue);
  18. assertNotEquals(differentValue, value);
  19. }
  20. }
  1. public interface ComparableContract<T extends Comparable<T>> extends Testable<T> {
  2. T createSmallerValue();
  3. @Test
  4. default void returnsZeroWhenComparedToItself() {
  5. T value = createValue();
  6. assertEquals(0, value.compareTo(value));
  7. }
  8. @Test
  9. default void returnsPositiveNumberWhenComparedToSmallerValue() {
  10. T value = createValue();
  11. T smallerValue = createSmallerValue();
  12. assertTrue(value.compareTo(smallerValue) > 0);
  13. }
  14. @Test
  15. default void returnsNegativeNumberWhenComparedToLargerValue() {
  16. T value = createValue();
  17. T smallerValue = createSmallerValue();
  18. assertTrue(smallerValue.compareTo(value) < 0);
  19. }
  20. }

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.

  1. class StringTests implements ComparableContract<String>, EqualsContract<String> {
  2. @Override
  3. public String createValue() {
  4. return "banana";
  5. }
  6. @Override
  7. public String createSmallerValue() {
  8. return "apple"; // 'a' < 'b' in "banana"
  9. }
  10. @Override
  11. public String createNotEqualValue() {
  12. return "cherry";
  13. }
  14. }
The above tests are merely meant as examples and therefore not complete.