Junit Timeouts

Junit: Timeouts

La anotación @Timeout permite declarar que una prueba, fábrica de pruebas, plantilla de pruebas o método de ciclo de vida debe fallar si su tiempo de ejecución excede una duración dada. La unidad de tiempo para la duración por defecto es segundos, pero es configurable.

El siguiente ejemplo muestra cómo se aplica @Timeout a métodos de ciclo de vida y de prueba.

class TimeoutDemo {

    @BeforeEach
    @Timeout(5)
    void setUp() {
        // fails if execution time exceeds 5 seconds
    }

    @Test
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
    void failsIfExecutionTimeExceeds500Milliseconds() {
        // fails if execution time exceeds 500 milliseconds
    }

    @Test
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS, threadMode = ThreadMode.SEPARATE_THREAD)
    void failsIfExecutionTimeExceeds500MillisecondsInSeparateThread() {
        // fails if execution time exceeds 500 milliseconds, the test code is executed in a separate thread
    }

}

Para aplicar el mismo tiempo de espera a todos los métodos de prueba dentro de una clase de prueba y todas sus clases @Nested, puedes declarar la anotación @Timeout a nivel de clase. Luego, se aplicará a todos los métodos de prueba, fábrica de pruebas y métodos de plantillas de prueba dentro de esa clase y sus clases @Nested, a menos que se sobrescriba mediante una anotación @Timeout en un método o clase @Nested específica. Ten en cuenta que las anotaciones @Timeout declaradas a nivel de clase no se aplican a los métodos de ciclo de vida.

Declarar @Timeout en un método @TestFactory verifica que el método de la fábrica se ejecute dentro de la duración especificada, pero no verifica el tiempo de ejecución de cada DynamicTest individual generado por la fábrica. Para ese propósito, usa assertTimeout() o assertTimeoutPreemptively().

Si @Timeout está presente en un método @TestTemplate — por ejemplo, en un @RepeatedTest o @ParameterizedTest — se aplicará el tiempo de espera a cada invocación de ese método.

Thread mode

El tiempo de espera se puede aplicar utilizando uno de los siguientes tres modos de hilo: SAME_THREAD, SEPARATE_THREAD o INFERRED.

  • SAME_THREAD: Cuando se utiliza este modo, la ejecución del método anotado se realiza en el hilo principal de la prueba. Si se supera el tiempo de espera, el hilo principal se interrumpe desde otro hilo. Esto se hace para garantizar la interoperabilidad con frameworks como Spring, que utilizan mecanismos sensibles al hilo en ejecución actual, como la gestión de transacciones con ThreadLocal.
  • SEPARATE_THREAD: En este caso, al igual que la aserción assertTimeoutPreemptively(), la ejecución del método anotado se realiza en un hilo separado. Esto puede provocar efectos secundarios no deseados, por lo que se debe tener cuidado al usar este modo. Consulta Preemptive Timeouts with assertTimeoutPreemptively() para más detalles.
  • INFERRED (predeterminado): Cuando se utiliza este modo de hilo, el modo de hilo se resuelve a través del parámetro de configuración junit.jupiter.execution.timeout.thread.mode.default. Si el parámetro de configuración proporcionado es inválido o no está presente, se utilizará SAME_THREAD como modo de hilo por defecto.

Default Timeouts

Los siguientes parámetros de configuración se pueden usar para especificar tiempos de espera predeterminados para todos los métodos de una categoría determinada, a menos que esos métodos o una clase de prueba envolvente estén anotados con @Timeout:

  • junit.jupiter.execution.timeout.default

    Tiempo de espera predeterminado para todos los métodos de prueba y de ciclo de vida.

  • junit.jupiter.execution.timeout.testable.method.default

    Tiempo de espera predeterminado para todos los métodos de prueba.

  • junit.jupiter.execution.timeout.test.method.default

    Tiempo de espera predeterminado para los métodos @Test.

  • junit.jupiter.execution.timeout.testtemplate.method.default

    Tiempo de espera predeterminado para los métodos @TestTemplate.

  • junit.jupiter.execution.timeout.testfactory.method.default

    Tiempo de espera predeterminado para los métodos @TestFactory.

  • junit.jupiter.execution.timeout.lifecycle.method.default

    Tiempo de espera predeterminado para todos los métodos de ciclo de vida.

  • junit.jupiter.execution.timeout.beforeall.method.default

    Tiempo de espera predeterminado para los métodos @BeforeAll.

  • junit.jupiter.execution.timeout.beforeeach.method.default

    Tiempo de espera predeterminado para los métodos @BeforeEach.

  • junit.jupiter.execution.timeout.aftereach.method.default

    Tiempo de espera predeterminado para los métodos @AfterEach.

  • junit.jupiter.execution.timeout.afterall.method.default

    Tiempo de espera predeterminado para los métodos @AfterAll.

Los parámetros de configuración más específicos sobrescriben a los menos específicos. Por ejemplo, junit.jupiter.execution.timeout.test.method.default sobrescribe junit.jupiter.execution.timeout.testable.method.default, que a su vez sobrescribe junit.jupiter.execution.timeout.default.

Los valores de estos parámetros de configuración deben tener el siguiente formato (sin distinción de mayúsculas o minúsculas): <número> [ns|μs|ms|s|m|h|d]. El espacio entre el número y la unidad es opcional. Si no se especifica ninguna unidad, se considera que el valor está en segundos.

Parameter value Equivalent annotation
42 @Timeout(42)
42 ns @Timeout(value = 42, unit = NANOSECONDS)
42 μs @Timeout(value = 42, unit = MICROSECONDS)
42 ms @Timeout(value = 42, unit = MILLISECONDS)
42 s @Timeout(value = 42, unit = SECONDS)
42 m @Timeout(value = 42, unit = MINUTES)
42 h @Timeout(value = 42, unit = HOURS)
42 d @Timeout(value = 42, unit = DAYS)

Using @Timeout for Polling Tests

Cuando se trata de código asincrónico, es común escribir pruebas que realicen sondeos mientras esperan que algo suceda antes de realizar cualquier afirmación. En algunos casos, puedes reescribir la lógica para usar un CountDownLatch o otro mecanismo de sincronización, pero a veces eso no es posible — por ejemplo, si el sujeto bajo prueba envía un mensaje a un canal en un corredor de mensajes externo y no se pueden realizar las afirmaciones hasta que el mensaje se haya enviado con éxito a través del canal. Las pruebas asincrónicas como estas requieren algún tipo de tiempo de espera para garantizar que no bloqueen el conjunto de pruebas ejecutándose indefinidamente, como sería el caso si un mensaje asincrónico nunca se entrega con éxito.

Al configurar un tiempo de espera para una prueba asincrónica que realiza sondeos, puedes asegurarte de que la prueba no se ejecute indefinidamente. El siguiente ejemplo demuestra cómo lograr esto con la anotación @Timeout de JUnit Jupiter. Esta técnica se puede usar para implementar lógica de “sondeo hasta” de manera muy sencilla.

@Test
@Timeout(5) // Poll at most 5 seconds
void pollUntil() throws InterruptedException {
    while (asynchronousResultNotAvailable()) {
        Thread.sleep(250); // custom poll interval
    }
    // Obtain the asynchronous result and perform assertions
}

❕Si necesita un mayor control sobre los intervalos de sondeo y una mayor flexibilidad con las pruebas asíncronas, considere el uso de una biblioteca dedicada como Awaitility..

Debugging Timeouts

Las extensiones de Pre-Interrupt Callback registradas se llaman antes de invocar Thread.interrupt() en el hilo que está ejecutando el método que ha excedido el tiempo de espera. Esto permite inspeccionar el estado de la aplicación y mostrar información adicional que podría ser útil para diagnosticar la causa de un tiempo de espera.

Thread Dump on Timeout

JUnit registra una implementación predeterminada del punto de extensión Pre-Interrupt Callback que volcará las pilas de todos los hilos a System.out si está habilitado mediante la configuración del parámetro junit.jupiter.execution.timeout.threaddump.enabled a true.

Disable @Timeout Globally

Cuando se está depurando el código en una sesión de depuración, un límite de tiempo fijo puede influir en el resultado de la prueba, por ejemplo, marcar la prueba como fallida aunque todas las afirmaciones se hayan cumplido.

JUnit Jupiter admite el parámetro de configuración junit.jupiter.execution.timeout.mode para configurar cuándo se aplican los tiempos de espera. Hay tres modos: enabled, disabled y disabled_on_debug. El modo predeterminado es enabled.

Un entorno de ejecución de la JVM se considera que está en modo depuración cuando uno de sus parámetros de entrada comienza con -agentlib:jdwp o -Xrunjdwp. Esta heurística es consultada por el modo disabled_on_debug.