Junit Test Interfaces And Default Methods

Junit: Test Interfaces and Default Methods

JUnit Jupiter permite que las anotaciones @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate, @BeforeEach y @AfterEach se declaren en métodos predeterminados de interfaces. Los métodos @BeforeAll y @AfterAll pueden declararse ya sea en métodos estáticos dentro de una interfaz de prueba o en métodos predeterminados de interfaz, siempre que la interfaz de prueba o la clase de prueba esté anotada con @TestInstance(Lifecycle.PER_CLASS) (consulta el ciclo de vida de la instancia de prueba). Aquí algunos ejemplos.

@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {

    static final 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(() -> String.format("About to execute [%s]",
            testInfo.getDisplayName()));
    }

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

}
interface TestInterfaceDynamicTestsDemo {

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

}

@ExtendWith y @Tag pueden ser declaradas en una interfaz de prueba, de modo que las clases que implementen dicha interfaz hereden automáticamente sus etiquetas y extensiones. Consulta Before and After Test Execution Callbacks para ver el código fuente de la TimingExtension.

@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}

En su clase de prueba puede implementar estas interfaces de prueba para que se apliquen.

class TestInterfaceDemo implements TestLifecycleLogger,
        TimeExecutionLogger, TestInterfaceDynamicTestsDemo {

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

}

Ejecutando el TestInterfaceDemo se obtiene una salida similar a la siguiente:

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

Otra posible aplicación de esta característica es escribir pruebas para contratos de interfaces. Por ejemplo, puedes escribir pruebas para cómo deben comportarse las implementaciones de Object.equals o Comparable.compareTo, de la siguiente manera.

public interface Testable<T> {

    T createValue();

}
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);
    }

}
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);
    }

}

En tu clase de prueba puedes implementar ambas interfaces de contrato y heredar así las pruebas correspondientes. Por supuesto, tendrás que implementar los métodos abstractos.

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";
    }

}

❕Las pruebas anteriores son meros ejemplos y, por tanto, no son completas.