web-dev-qa-db-fra.com

Jest: Timer et Promise ne fonctionnent pas bien. (setTimeout et fonction asynchrone)

Des idées sur ce code

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

`` `

A échoué avec

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

Cependant, si je supprime await de LINE-A, le test réussit.

Est-ce que Promise and Timer ne fonctionne pas bien?

Je pense que la raison pour laquelle la plaisanterie attend peut-être une seconde promesse est résolue.

19
GutenYe

Oui, vous êtes sur la bonne voie.


Que se passe-t-il

await simpleTimer(callback) attendra que la promesse retournée par simpleTimer() soit résolue afin que callback() soit appelé pour la première fois et que setTimeout() soit également appelé. jest.useFakeTimers()a remplacé setTimeout() par un simulacre de sorte que le simulacre enregistre qu'il a été appelé avec [ () => { simpleTimer(callback) }, 1000 ].

jest.advanceTimersByTime(8000) exécute () => { simpleTimer(callback) } (Depuis 1000 <8000) qui appelle setTimer(callback) qui appelle callback() une deuxième fois et renvoie la promesse créée par await . setTimeout() ne s'exécute pas une seconde fois depuis le reste de setTimer(callback)est mis en file d'attente dans la file d'attente PromiseJobs et n'a pas eu la chance de s'exécuter.

expect(callback).toHaveBeenCalledTimes(9) échoue en signalant que callback() n'a été appelé que deux fois.


Informations complémentaires

C'est une bonne question. Il attire l'attention sur certaines caractéristiques uniques de JavaScript et sur son fonctionnement sous le capot.

Message Queue

JavaScript utilise ne file de messages . Chaque message est exécution complète avant que le moteur d'exécution revienne dans la file d'attente pour récupérer le message suivant. Fonctions comme setTimeout()ajouter des messages à la file d'attente .

Files d'attente de travaux

ES6 introduit Job Queues et l'une des files d'attente de travaux requises est PromiseJobs, qui gère les "travaux en réponse au règlement d'une promesse". Tous les travaux de cette file d'attente sont exécutés une fois le message actuel terminé et avant que le message suivant ne commence. then() met en file d'attente un travail dans PromiseJobs lorsque la promesse sur laquelle il est appelé est résolue.

async/wait

async / awaitn'est qu'un sucre syntaxique sur les promesses et les générateurs . async renvoie toujours une promesse et await englobe essentiellement le reste de la fonction dans un rappel then attaché à la promesse donnée.

Minuterie se moque

Timer Mocks fonctionne par remplace des fonctions comme setTimeout() par des mocks lorsque jest.useFakeTimers() est appelé. Ces simulacres enregistrent les arguments qui les ont appelés. Puis, lorsque jest.advanceTimersByTime() est appelé une boucle qui appelle de manière synchrone tous les rappels qui auraient été planifiés dans le temps écoulé, y compris ceux ajoutés lors de l'exécution des rappels.

En d'autres termes, setTimeout() met normalement en file d'attente les messages qui doivent attendre que le message actuel se termine avant de pouvoir être exécutés. Les minuteries permettent aux rappels d'être exécutés de manière synchrone dans le message en cours.

Voici un exemple illustrant les informations ci-dessus:

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

Comment obtenir des simulacres et des promesses de jouer à Nice

Les temporisations simulées exécuteront les rappels de manière synchrone, mais ces rappels peuvent entraîner la mise en file d'attente des travaux dans PromiseJobs.

Heureusement, il est en fait assez facile de laisser tous les travaux en attente dans PromiseJobs s'exécuter dans un test async, il vous suffit d'appeler await Promise.resolve(). Cela va essentiellement mettre le reste du test en file d'attente à la fin de la file d'attente PromiseJobs et laisser tout s'exécuter en premier.

Dans cet esprit, voici une version de test du test:

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

Il y a un cas d'utilisation pour lequel je n'ai pas trouvé de solution:

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

Et le test ressemble à:

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

Fondamentalement, l'action ne sera pas résolue à moins que le minuteur avance. On se croirait dans une dépendance circulaire ici: une minuterie promise doit avancer pour résoudre, une minuterie factice doit être résolue pour avancer.

1
nemo