javascript testing jestjs

javascript - Broma: el temporizador y la promesa no funcionan bien.(setTimeout y función asíncrona)



testing jestjs (2)

Hay un caso de uso que simplemente no pude encontrar una solución:

function action(){ return new Promise(function(resolve, reject){ let poll (function run(){ callAPI().then(function(resp){ if (resp.completed) { resolve(response) return } poll = setTimeout(run, 100) }) })() }) }

Y la prueba se parece a:

jest.useFakeTimers() const promise = action() // jest.advanceTimersByTime(1000) // this won''t work because the timer is not created await expect(promise).resolves.toEqual(({completed:true}) // jest.advanceTimersByTime(1000) // this won''t work either because the promise will never resolve

Básicamente, la acción no se resolverá a menos que el temporizador avance. Se siente como una dependencia circular aquí: la promesa necesita un temporizador para avanzar para resolver, el falso temporizador necesita una promesa para resolver para avanzar.

Cualquier idea en este código

jest.useFakeTimers() it(''simpleTimer'', async () => { async function simpleTimer(callback) { await callback() // LINE-A without await here, test works as expected. setTimeout(() => { simpleTimer(callback) }, 1000) } const callback = jest.fn() await simpleTimer(callback) jest.advanceTimersByTime(8000) expect(callback).toHaveBeenCalledTimes(9) }

`` `

Falló con

Expected mock function to have been called nine times, but it was called two times.

Sin embargo, si quito await de LINE-A, la prueba pasa.

¿Promise and Timer no funciona bien?

Creo que la razón tal vez la broma está esperando la segunda promesa para resolver.


Sí, estás en el camino correcto.

Lo que pasa

await simpleTimer(callback) esperará a que la Promesa devuelta por simpleTimer() resuelva para que se simpleTimer() callback() la primera vez y también se llame a setTimeout() . jest.useFakeTimers() reemplazó a setTimeout() con un simulacro, de modo que el simulacro registra que fue llamado con [ () => { simpleTimer(callback) }, 1000 ] .

jest.advanceTimersByTime(8000) ejecuta () => { simpleTimer(callback) } (desde 1000 <8000) que llama a setTimer(callback) que llama a callback() la segunda vez y devuelve la Promesa creada por await . setTimeout() no se ejecuta una segunda vez, ya que el resto de setTimer(callback) está en cola en la cola de PromiseJobs y no ha tenido la oportunidad de ejecutarse.

expect(callback).toHaveBeenCalledTimes(9) falla al informar que callback() solo se llamó dos veces.

Información Adicional

Esta es una buena pregunta. Llama la atención sobre algunas características únicas de JavaScript y cómo funciona bajo el capó.

Cola de mensajes

JavaScript utiliza una cola de mensajes . Cada mensaje se ejecuta hasta su finalización antes de que el tiempo de ejecución vuelva a la cola para recuperar el mensaje siguiente. Funciones como setTimeout() agregan mensajes a la cola .

Colas de trabajo

ES6 introduce las Job Queues y una de las colas de trabajo requeridas es PromiseJobs que maneja "los trabajos que son respuestas a la liquidación de una promesa". Todos los trabajos en esta cola se ejecutan después de que se complete el mensaje actual y antes de que comience el siguiente mensaje . then() PromiseJobs en cola un trabajo en PromiseJobs cuando se PromiseJobs la Promesa a la que se llama.

asíncrono / espera

async / await es solo azúcar sintáctica sobre promesas y generadores . async siempre devuelve una Promesa y await esencialmente envuelve el resto de la función en una devolución de llamada adjunta a la Promesa que se le da.

Temporizador Mocks

Las simulaciones de temporizador funcionan al reemplazar funciones como setTimeout() con jest.useFakeTimers() cuando se llama a jest.useFakeTimers() . Estos simulacros registran los argumentos con los que fueron llamados. Luego, cuando se llama jest.advanceTimersByTime() se ejecuta un bucle que sincrónicamente llama a cualquier devolución de llamada que se hubiera programado en el tiempo transcurrido, incluidos los que se agregan mientras se ejecutan las devoluciones de llamada.

En otras palabras, setTimeout() normalmente setTimeout() cola los mensajes que deben esperar hasta que el mensaje actual se complete antes de que puedan ejecutarse. Los temporizadores de temporizador permiten que las devoluciones de llamada se ejecuten de forma síncrona dentro del mensaje actual.

Aquí hay un ejemplo que demuestra la información anterior:

jest.useFakeTimers(); test(''execution order'', async () => { const order = []; order.push(''1''); setTimeout(() => { order.push(''6''); }, 0); const promise = new Promise(resolve => { order.push(''2''); resolve(); }).then(() => { order.push(''4''); }); order.push(''3''); await promise; order.push(''5''); jest.advanceTimersByTime(0); expect(order).toEqual([ ''1'', ''2'', ''3'', ''4'', ''5'', ''6'' ]); });

Cómo conseguir que Timer Mocks y Promises jueguen bien

Timer Mocks ejecutará las devoluciones de llamada de forma síncrona, pero esas devoluciones de llamada pueden hacer que los trabajos se PromiseJobs en cola en PromiseJobs .

Afortunadamente, en realidad es bastante fácil dejar que todos los trabajos pendientes en PromiseJobs ejecuten dentro de una prueba async , todo lo que debe hacer es llamar a await Promise.resolve() . Básicamente, esto pondrá en cola el resto de la prueba al final de la cola PromiseJobs y permitirá que todo lo que ya esté en la cola se ejecute primero.

Con eso en mente, aquí hay una versión de trabajo de la prueba:

jest.useFakeTimers() it(''simpleTimer'', async () => { async function simpleTimer(callback) { await callback(); setTimeout(() => { simpleTimer(callback); }, 1000); } const callback = jest.fn(); await simpleTimer(callback); for(let i = 0; i < 8; i++) { jest.advanceTimersByTime(1000); await Promise.resolve(); // allow any pending jobs in the PromiseJobs queue to run } expect(callback).toHaveBeenCalledTimes(9); // SUCCESS });