Testing Asynchronous Code
Es común en JavaScript ejecutar código de forma asincrónica. Si tienes código que se ejecuta de forma asincrónica, Jest debe saber cuando ha terminado el código que se está probando, antes de que puede pasar a otra test. Jest tiene varias formas de manejar esto.
Callbacks
El patrón asincrónico más común es los callbacks.
Por ejemplo, supongamos que tienes una función fetchData(callback)
que trae algunos datos y llama a callback(data)
cuando esta completada. You want to test that this returned data is the string 'peanut butter'
.
Por defecto, Jest da por completados los tests una vez que llegan al final de su ejecución. Esto significa que este test no funciona como estaba previsto:
// Don't do this!
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
El problema es que el test terminará tan pronto como fetchData
finalize, antes de llamar a la funcion callback.
Hay una forma alternativa de test
que corrige esto. En vez de poner el test en una función con un argumento vacío, utilice un solo argumento llamado done
. Jest esperará hasta que se llame el callback de done
antes de terminar la prueba.
test('the data is peanut butter', done => {
function callback(data) {
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
If done()
is never called, the test will fail (with timeout error), which is what you want to happen.
If the expect
statement fails, it throws an error and done()
is not called. If we want to see in the test log why it failed, we have to wrap expect
in a try
block and pass the error in the catch
block to done
. Otherwise, we end up with an opaque timeout error that doesn't show what value was received by expect(data)
.
Promesas
If your code uses promises, there is a more straightforward way to handle asynchronous tests. Return a promise from your test, and Jest will wait for that promise to resolve. Si se rechaza la promesa, la prueba fallará automáticamente.
Por ejemplo, digamos que fetchData
, en lugar de usar un "callback", devuelve una promesa que supuestamente resolverá a la cadena de texto 'peanut butter'
. Podríamos testearlo con:
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
Be sure to return the promise - if you omit this return
statement, your test will complete before the promise returned from fetchData
resolves and then() has a chance to execute the callback.
If you expect a promise to be rejected, use the .catch
method. Asegúrate de añadir expect.assertions
para verificar que un cierto número de afirmaciones están siendo llamadas. Otherwise, a fulfilled promise would not fail the test.
test('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(e => expect(e).toMatch('error'));
});
.resolves
/ .rejects
También puede utilizar el marcador .resolves
en tu declaración de "expect" y Jest esperará a que esa promesa resuelva. Si se rechaza la promesa, el test fallará automáticamente.
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
Be sure to return the assertion—if you omit this return
statement, your test will complete before the promise returned from fetchData
is resolved and then() has a chance to execute the callback.
If you expect a promise to be rejected, use the .rejects
matcher. Actúa análogamente al marcador .resolves
. Si se cumple la promesa, el test fallará automáticamente.
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
Async/Await
Como alternativa, se puede usar async
y await
en tests. To write an async test, use the async
keyword in front of the function passed to test
. Por ejemplo, puede probarse el mismo escenario fetchData
con:
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
expect(e).toMatch('error');
}
});
You can combine async
and await
with .resolves
or .rejects
.
test('the data is peanut butter', async () => {
await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
await expect(fetchData()).rejects.toThrow('error');
});
In these cases, async
and await
are effectively syntactic sugar for the same logic as the promises example uses.
None of these forms is particularly superior to the others, and you can mix and match them across a codebase or even in a single file. It just depends on which style you feel makes your tests simpler.