Jest: Fournir une API pour vider la file d'attente de résolution Promise

Créé le 23 nov. 2016  ·  46Commentaires  ·  Source: facebook/jest

Vous souhaitez demander une fonctionnalité ou signaler un bug ?

_Feature_, je suppose, mais assez importante lors du test de code qui utilise Promise s.

Quel est le comportement actuel ?

J'ai un composant qui utilise l'emballage et le chaînage Promise interne dans le suivi d'une action asynchrone externe. Je fournis la simulation de l'action asynchrone et résout la promesse qu'elle renvoie dans mon test.

Le composant quelque chose comme ceci :

class Component extends React.Component {
  // ...
  load() {
    Promise.resolve(this.props.load())
      .then(
        result => result
          ? result
          : Promise.reject(/* ... */)
        () => Promise.reject(/* ... */)
      )
      .then(result => this.props.afterLoad(result));
  }
}

Et le code de test ressemble à ceci :

const load = jest.fn(() => new Promise(succeed => load.succeed = succeed));
const afterLoad = jest.fn();
const result = 'mock result';
mount(<Component load={load} afterLoad={afterLoad} />);
// ... some interaction that requires the `load`
load.succeed(result);
expect(afterLoad).toHaveBeenCalledWith(result);

Le test échoue car le expect() est évalué avant les gestionnaires de promesses chaînées. Je dois reproduire la longueur de la chaîne de promesse interne dans le test pour obtenir ce dont j'ai besoin, comme ceci :

return Promise.resolve(load.succeed(result))
  // length of the `.then()` chain needs to be at least as long as in the tested code
  .then(() => {})
  .then(() => expect(result).toHaveBeenCalledWith(result));

Quel est le comportement attendu ?

Je m'attendrais à ce que Jest fournisse une sorte d'API pour vider tous les gestionnaires de promesses en attente, par exemple :

load.succeed(result);
jest.flushAllPromises();
expect(result).toHaveBeenCalledWith(result);

J'ai essayé runAllTicks et runAllTimers sans effet.


_Alternativement, s'il me manque juste une fonctionnalité ou un modèle déjà existant, j'espère que quelqu'un ici me dirigera dans la bonne direction :)_

Enhancement New API proposal

Commentaire le plus utile

Une fonction d'assistance peut transformer cela en une promesse elle-même afin que vous n'ayez pas besoin de vous occuper du rappel effectué. C'est assez petit pour qu'il soit assez inoffensif à garder dans l'espace utilisateur, mais je ne me plaindrais pas s'il était mis sur l'objet de plaisanterie. Quelque chose comme ça est beaucoup utilisé dans mes projets.

function flushPromises() {
  return new Promise(resolve => setImmediate(resolve));
}

test('', () => {
  somethingThatKicksOffPromiseChain();
  return flushPromises().then(() => {
    expect(...
  });
})

Avec async wait c'est presque joli :

test('', async () => {
  somethingThatKicksOffPromiseChain();
  await flushPromises();
  expect(...
})

Tous les 46 commentaires

Alors que les tests asynchrones promettent, il est bon de se rappeler que vous pouvez renvoyer une fonction de test en tant que promesse, donc quelque chose comme ceci fonctionnera :

test('my promise test', () => { //a test function returning a Promise
  return Promise.resolve(load.succeed(result))
    .then(() => {})
    .then(() => expect(result).toHaveBeenCalledWith(result));
})

Le renvoi d'une promesse à partir de la fonction de test permet à Jest de se rendre compte qu'il s'agit d'un test asynchrone et d'attendre qu'il soit résolu ou expire.

@thymikee Bien sûr, je renvoie la valeur pour faire attendre Jest - c'est complètement .then(() => {}) dans votre code. Je ne vois pas comment je peux décrire le problème de manière plus concise que je ne l'ai déjà fait dans le post d'ouverture. Veuillez le lire attentivement et rouvrir le problème ou décrire comment le contourner.

_J'ai ajouté le return au code dans l'OP pour éviter toute confusion._

A rencontré un problème similaire et l'a décrit ici : https://github.com/pekala/test-problem-example

En bref : j'essaie d'affirmer la séquence d'actions envoyée au magasin Redux à la suite de l'interaction de l'utilisateur (simulée à l'aide d'une enzyme). Les actions envoyées sont synchronisées et asynchrones à l'aide de Promises (simulées pour être résolues immédiatement). Il semble qu'il n'y ait aucun moyen d'affirmer une fois la chaîne de promesse épuisée, si vous n'avez pas un accès direct à la chaîne de promesse. setTimeout(..., 0) fonctionne, mais cela semble difficile et si l'assertion dans le rappel de setTimeout échoue, Jest échoue avec une erreur de délai d'attente (au lieu d'une erreur d'assertion).

L'idée de flushAllPromises semble être une solution, même si je pense que c'est ce que runAllTicks devrait faire ?

En guise de suivi : j'ai essayé de remplacer setTimeout(..., 0) par setImmediate et cela semble à la fois exécuter les assertions une fois que la file d'attente des microtâches de rappel Promise est épuisée et empêche Jest de expirer en cas d'erreur d'assertion. Donc, cela fonctionne bien et est une solution acceptable pour mon cas d'utilisation :

test('changing the reddit downloads posts', done => {
    setImmediate(() => {
        // assertions...
        done()
    })
})

Une fonction d'assistance peut transformer cela en une promesse elle-même afin que vous n'ayez pas besoin de vous occuper du rappel effectué. C'est assez petit pour qu'il soit assez inoffensif à garder dans l'espace utilisateur, mais je ne me plaindrais pas s'il était mis sur l'objet de plaisanterie. Quelque chose comme ça est beaucoup utilisé dans mes projets.

function flushPromises() {
  return new Promise(resolve => setImmediate(resolve));
}

test('', () => {
  somethingThatKicksOffPromiseChain();
  return flushPromises().then(() => {
    expect(...
  });
})

Avec async wait c'est presque joli :

test('', async () => {
  somethingThatKicksOffPromiseChain();
  await flushPromises();
  expect(...
})

@jwbay c'est du bon sucre juste là 🍠 !

Il est vrai que ce flushPromises s'avère être une ligne unique, mais il n'est pas du tout évident de savoir comment accéder à cette ligne. Je pense donc que ce serait un avantage pour les utilisateurs de Jest de l'avoir disponible en tant que fonction util.

@pekala the one liner IMO ne fournit pas le comportement requis car il n'attendra pas que la promesse en attente suivante soit résolue :

function foo() {  
  return new Promise((res) => {
    setTimeout(() => {
      res()
    }, 2000);
  });
}

Qu'en est-il de la promesse swizzling et lorsqu'une nouvelle promesse est créée, ajoutez-la à un tableau, puis videz toutes les promesses qui attendront sur Promise.all sur ce tableau ?

@talkol Je pense que ce sera le cas, tant que vous utiliserez également les fausses minuteries. Je n'ai pas testé ça par contre.

@pekala pas besoin de
J'ai juste peur que la promesse bouillonnante perturbe le fonctionnement interne de la plaisanterie, c'est un noyau un peu dur

Si vous ne simulez pas de minuteurs, vos tests prendront de vrais 2s + pour terminer. Je pense que la meilleure pratique serait de supprimer ces types de retards, auquel cas le flushPromises proposé par @jwbay fait le travail.

Tout dépend de ce que vous essayez de tester :) Tout ce que je dis, c'est que les minuteries ne sont pas liées à l'attente des promesses

Nous rencontrons des problèmes liés aux promesses non résolues, qui sont entremêlés avec des appels setTimeout. Dans jest v19.0.2, nous n'avons aucun problème, mais dans jest v20.0.0, les promesses n'entrent jamais dans les fonctions de résolution/rejet et les tests échouent donc. Notre problème semble être lié à ce problème de ne pas avoir _une API pour vider la file d'attente de résolution de promesse_, mais ce problème semble être antérieur à la v20.0.0 où nous avons commencé à voir le problème, donc je ne suis pas tout à fait sûr.

C'est la seule solution que nous avons pu trouver pour certains de nos tests, car nous avons une série de setTimeout s et Promise alternance utilisés dans le code qui appelle finalement le onUpdateFailed rappel.

  ReactTestUtils.Simulate.submit(form);
  return Promise.resolve()
    .then(() => { jest.runOnlyPendingTimers(); })
    .then(() => { jest.runOnlyPendingTimers(); })
    .then(() => { jest.runOnlyPendingTimers(); })
    .then(() => {
      expect(onUpdateFailed).toHaveBeenCalledTimes(1);
      expect(getErrorMessage(page)).toEqual('Input is invalid.');
    });

Pas si joli, donc tout conseil ici très apprécié.

Un autre exemple où vous ne pouvez pas retourner la promesse du test :

describe('stream from promise', () => {
  it('should wait till promise resolves', () => {
    const stream = Observable.fromPromise(Promise.resolve('foo'));
    const results = [];
    stream.subscribe(data => { results.push(data); });
    jest.runAllTimers();
    expect(results).toEqual(['foo']);
  });
});

Ce test échoue avec jest 20.0.4.

La solution de @philwhln peut également être écrite avec async/await

ReactTestUtils.Simulate.submit(form);

await jest.runOnlyPendingTimers();
await jest.runOnlyPendingTimers();
await jest.runOnlyPendingTimers();

expect(onUpdateFailed).toHaveBeenCalledTimes(1);
expect(getErrorMessage(page)).toEqual('Input is invalid.');

J'aimerais une fonction utilitaire qui vide la file d'attente des promesses

J'aimerais une fonction qui vide également les files d'attente de promesses entre les tests.

Je teste du code qui utilise Promise.all pour envelopper plusieurs promesses. Lorsqu'une de ces promesses encapsulées échoue (parce que c'est ce que je veux tester), la promesse revient immédiatement, ce qui signifie que les autres promesses reviennent parfois (condition de concurrence, non déterministe) pendant que le test suivant est en cours d'exécution.

Cela provoque toutes sortes de ravages avec mes tests ayant des résultats non prévisibles/répétables.

Pour implémenter correctement cela, nous aurions besoin de nous moquer de Promise afin que nous puissions éventuellement voir toutes les micro-tâches en file d'attente pour les résoudre de manière synchrone. Quelque chose dans le sens de ce que fait la fausse promesse .

Il existe déjà une API pour vider les micro-tâches mises en file d'attente avec process.nextTick et cette API devrait probablement également fonctionner avec Promises ( jest.runAllTicks ).

J'avais une solution avec Jasmine qui s'accrochait au nextTick de Yaku, une bibliothèque de promesses et captait les appels nextTick et permettait de les jouer tôt.
Cependant, la plaisanterie utilise elle-même les promesses, ce qui a rendu cela problématique.
En fin de compte, j'ai pris Yaku et je l'ai piraté pour avoir une méthode de vidage qui vide sa file d'attente. Par défaut, il s'exécute normalement en utilisant nextTick, mais si vous appelez flush, tous les gestionnaires de promesses en attente s'exécutent.
La source est ici :
https://github.com/lukeapage/yaku-mock
Cela pourrait faire du rangement, contacter ysmood pour voir ce qu'ils en pensent et ajouter de la documentation, mais cela fait à peu près ce que vous voulez et a fonctionné pour moi comme une solution simple pour faire des promesses de synchronisation dans les tests.

Comme solution de contournement simple, j'aime la solution de

Et si nous ajoutions quelque chose de similaire à l'objet jest ?

await jest.nextTick();

Mis en œuvre comme

const nextTick = () => new Promise(res => process.nextTick(res));

cc @cpojer @SimenB @rogeliog

J'utilise une enzyme pour monter des composants React.

Moi aussi, j'ai des fonctions qui s'attendent à ce que Promises s'exécute, mais aucun des correctifs susmentionnés n'a fonctionné. Je serais capable de les gérer de manière synchrone dans mon test - si - les fonctions ont renvoyé les objets Promise, en utilisant await , mais malheureusement, les fonctions ne renvoient pas les objets Promise.

C'est la solution de contournement que j'ai fini par faire en utilisant un espion sur la fonction Promise globale.

global.Promise = require.requireActual('promise');

it('my test', async () => {
    const spy = sinon.spy(global, 'Promise');

    wrapper.props().dispatch(functionWithPromiseCalls());

    for (let i = 0; i < spy.callCount; i += 1) {
      const promise = spy.getCall(i);
      await promise.returnValue;
    }

    expect(...)
});

J'ai rencontré un cas d'utilisation pour cela (merci @jwbay pour la technique géniale)

Par exemple, vous voulez vérifier que votre fonction a un délai d'attente, et que le délai d'attente est précisément appliqué :

      jest.useFakeTimers();
      const EXPECTED_DEFAULT_TIMEOUT_MS = 10000;

      const catchHandler = jest.fn().mockImplementationOnce(err => {
        expect(err).not.toBeNull();
        expect(err.message).toContain('timeout');
      });

      // launch the async func returning a promise
      fetchStuffWithTimeout().catch(catchHandler);

      expect(catchHandler).not.toHaveBeenCalled(); // not yet

      jest.advanceTimersByTime(EXPECTED_DEFAULT_TIMEOUT_MS - 1);
      await flushPromises();

      expect(catchHandler).not.toHaveBeenCalled(); // not yet

      jest.advanceTimersByTime(1);
      await flushPromises();

      expect(catchHandler).toHaveBeenCalledTimes(1); // ok, rejected precisely

renvoyer une promesse ne permet pas de vérifier le timing précis de la résolution/rejet.

Une promesse de rinçage s'impose là. Sans cela, l'attente est appelée trop tôt.

J'espère que cela aide à cerner le problème.

Pour les personnes qui suivent, il y a un PR ouvert pour cela ici : #6876

Publication croisée de https://github.com/airbnb/enzyme/issues/1587

Je me demande si le modèle suivant devrait suffire à résoudre ce problème, et si je fais quelque chose qui est considéré comme une mauvaise pratique et que je ne devrais pas faire.

Que pensent les gens de cette approche ?

export class MyComponent extends React.Component {
  constructor (props) {
    super(props)

    this.hasFinishedAsync = new Promise((resolve, reject) => {
      this.finishedAsyncResolve = resolve
    })
  }

  componentDidMount () {
    this.doSomethingAsync()
  }

  async doSomethingAsync () {
    try {
      actuallyDoAsync()
      this.props.callback()
      this.finishedAsyncResolve('success')
    } catch (error) {
      this.props.callback()
      this.finishedAsyncResolve('error')
    }
  }

  // the rest of the component
}

Et dans les tests :

it(`should properly await for async code to finish`, () => {
  const mockCallback = jest.fn()
  const wrapper = shallow(<MyComponent callback={mockCallback}/>)

  expect(mockCallback.mock.calls.length).toBe(0)

  await wrapper.instance().hasFinishedAsync

  expect(mockCallback.mock.calls.length).toBe(1)
})

J'ai eu un problème lorsque l'appel asynchrone n'était pas effectué directement dans componentDidMount, mais il appelait une fonction asynchrone, qui appelait une autre fonction asynchrone, etc. Si j'ajoutais une étape asynchrone supplémentaire dans toute la chaîne asynchrone, je devrais ajouter un .then() supplémentaire ou un await supplémentaire, mais cela fonctionne très bien.

Y a-t-il une raison pour laquelle je ne devrais pas utiliser cette approche ou est-ce que cela semble bon pour les gens ?

Je suis parti à l'aventure en faisant cela dans l'espace utilisateur et j'ai trouvé que c'était en fait faisable et pas si mal (bien qu'il y ait pas mal de pièges à rencontrer si vous n'avez pas de carte). Voici un rapport d'expérience qui est (espérons-le) suffisamment détaillé pour être utilisé directement ; un TLDR consiste à transpiler async / await en promesses et à échanger les promesses natives contre bluebird et les minuteries natives contre lolex ; tout transpiler , y compris node_modules/ ; queueMicrotask est la primitive dont vous avez besoin pour les promesses, mais par défaut, Lolex ne la fournira pas car JSDOM ne la fournit pas.

J'ai rencontré le même problème avec les composants jest.mockAllTimers() et React qui appellent un Promise dans componentDidMount() .

La solution de #issuecomment-279171856 a résolu le problème de manière élégante.

Nous avons besoin de quelque chose de similaire dans l'API Jest officielle !

J'ai récemment rencontré un problème lors de la mise à niveau d'un tas de choses, cela a révélé un problème dans un tas de tests où nous n'attendions pas toujours les promesses de finir. Et tandis que des méthodes comme await new Promise(resolve => setImmediate(resolve)); fonctionnaient dans des cas simples, j'ai découvert dans mes tests que je devais l'exécuter plusieurs fois pour effacer le tuyau. C'est ce que @quasicomputational a mentionné dans leur exploration ici . Malheureusement, je ne pense pas qu'il existe un moyen de savoir quand ce tuyau est clair sans intercepter les promesses au fur et à mesure qu'elles sont créées. J'ai donc créé une petite bibliothèque pour faire ça... promise-spy . Cependant, j'ai eu un test qui utilisait de fausses minuteries et cela n'a pas fonctionné avec ça... donc ce n'est pas encore une solution entièrement fonctionnelle.

Bien que j'imagine aussi qu'ils ne peuvent fonctionner qu'avec async / await alls dans votre code à tester SI ils sont transpilés en promesses. Si elles ne sont pas transpilées en promesses, alors cette bibliothèque ne pourra pas s'y connecter et attendre qu'elles se terminent.

Je me suis retrouvé avec le même problème et j'ai réalisé:
nous ne devrions pas vider les promesses en attente, mais à la place, nous devrions faire échouer tout le test s'il y a des promesses en attente.
De cette façon, nous serons obligés d'abandonner les promesses en attente à l'intérieur du code testé à l'aide du contrôleur Abort :
https://developers.google.com/web/updates/2017/09/abortable-fetch
Avoir des promesses en plaisantant équivaut à dire « La concurrence est difficile, alors ne la testons pas ». En réalité, ce devrait être tout le contraire.
Étant donné que la concurrence est difficile, nous devrions la tester encore plus et ne pas autoriser du tout la réussite d'un test avec des promesses en attente.

Étant donné le désordre lié à l'abandon des promesses sur cette question de Stackoverflow, il est clair que ce n'est pas (ENCORE) une chose facile à faire :
https://stackoverflow.com/a/53933849/373542
Je vais essayer d'écrire une implémentation KISS de l'abandon de mes promesses de récupération et posterai le résultat ici.

@giorgio-zamparelli: _"La simultanéité est difficile, alors ne la testons pas"_ est complètement hors de propos du rapport d'origine. Le problème ne concerne pas les promesses _pending_ mais plutôt le fait qu'attendre la propagation de la _résolution_ promise via le code asynchrone dans les tests est inutilement difficile.

Je pense que les promesses de rinçage seraient de guérir les symptômes au lieu de la maladie.

Les promesses devraient se résoudre normalement dans les tests sans avoir besoin d'être vidés.
S'il y a une promesse en attente dans votre test, vous devez soit attendre qu'elle se résolve en utilisant par exemple wait de @testing-library/react OU si la promesse en attente ne fait pas partie de la portée du test, vous devez soit vous moquez du code en le démarrant ou vous devriez annuler la promesse en attente quelque part comme sur l'événement de cycle de vie React willUnmount en utilisant le AbortController

L'AbortController est une nouvelle API que presque personne n'utilise et j'ai le sentiment qu'elle est la solution à la plupart des promesses suspendues dans les tests.

PROUVE MOI LE CONTRAIRE:
Je pourrais facilement me tromper si quelqu'un qui a signalé avoir des problèmes avec des problèmes en attente dans ce problème a déjà essayé d'utiliser AbortController et jest.mock et ce n'était pas suffisant.

@giorgio-zamparelli: Peut-être que le malentendu provient de mon utilisation de l'expression _"vider tous les gestionnaires de promesses en attente"_ (et si c'est le cas, je suis désolé). Comme vous le verrez si vous lisez attentivement la description du problème, je voulais dire "gestionnaires de promesses en attente".

Donc, pour réitérer, nous ne parlons pas ici de promesses en attente (en aucune façon), mais plutôt de purger la résolution de promesse avec un minimum de tracas. Ou, en d'autres termes, de passer de manière transparente et déterministe du point où une promesse est résolue au point où tous les effets ultérieurs qui y sont liés sont invoqués (afin que nous puissions en tester le résultat).

J'ai récemment publié flush-microtasks à cet effet. Elle emprunte sa mise en @jwbay « solution s ici ou @thymikee » solution s ici . Je ne sais pas si la complexité fait une différence significative, mais je suppose que cela tient compte des cas limites non pris en compte par les autres solutions de ce fil. Je n'ai utilisé cette implémentation que parce que react-testing-library utilise (voir ici ), mais ne l'expose pas.

import { flushMicroTasks } from 'flush-microtasks'

await flushMicroTasks()

@aleclarson Y a-t-il une différence entre les micro-tâches de rinçage et

@ramusus On dirait que flush-promises utilise la même approche que la solution de @jwbay .

https://github.com/kentor/flush-promises/blob/46f58770b14fb74ce1ff27da00837c7e722b9d06/index.js

RTL a également copié le code de React : https://github.com/testing-library/react-testing-library/blob/8db62fee6303d16e0d5c933ec1fab5841dd2109b/src/flush-microtasks.js

EDIT : hah, déjà mentionné :grinning:

Je ne suis pas sûr que nous ayons besoin de l'intégrer dans Jest lorsque les gens peuvent l'utiliser ? Peut-être qu'on peut y faire un lien dans la doc ? Ce problème consiste à les vider de manière synchrone, ce qui, je pense, va au-delà de ce que nous voulons faire (d'autant plus que c'est impossible avec async-await )

La solution flushPromises ne fonctionne que sur les Promesses qui sont résolues immédiatement mais pas sur celles qui sont toujours en attente.

Hum, bonne remarque. Je ne sais pas s'il est possible de suivre les promesses de pending manière ou d'une autre. Pourrait être capable de faire quelque chose d'intelligent avec async_hooks , pas sûr. Cela va probablement être pénible d'essayer de faire la différence entre les promesses créées par le code utilisateur et les promesses créées par Jest et ses dépendances

J'ai essayé d'envelopper/moquer l'objet Promise pour inclure un compteur mais cela ne fonctionne pas :

const _promise = window.Promise;
window.Promise = function(promiseFunction){
    // counter
    return new _promise(promiseFunction);
}

Le problème principal réside dans les fonctions async qui n'utilisent pas du tout le Promise global

Ok, j'ai trouvé un moyen vraiment hack comme celui-ci .

  1. Créez un nouveau module avec une liste.
  2. Ajoutez vos promesses à cette liste.
  3. Résolvez les promesses de votre test et supprimez-les de la liste.
  4. Dans mon cas, je lance wrapper.update() partir d'enzyme. Faites ici quelque chose de similaire si nécessaire.
  5. Répétez les étapes 3 et 4 jusqu'à ce que la liste soit vide.

Je sais, ce n'est pas une bonne pratique d'ajuster le code aux tests MAIS j'utilise déjà cette logique sur le rendu côté serveur. Mais au final, il n'y a qu'à attendre. \_(ツ)_/¯

Il y a une mise à jour intéressante à ce sujet dans Jest 26, où les faux minuteurs sont désormais basés sur @sinon/fake-timers (si activé avec jest.useFakeTimers('modern') ).

J'ai essayé les fausses minuteries modernes avec mes tests, et malheureusement, le hack await new Promise(resolve => setImmediate(resolve)); se bloque indéfiniment. Heureusement, @sinon/fake-timers inclut plusieurs méthodes *Async() qui "casseront également la boucle d'événement, permettant à tout rappel de promesse planifié d'exécuter _avant_ d'exécuter les minuteries.". Malheureusement, je ne vois aucun moyen d'obtenir l'objet clock via les API Jest.

Quelqu'un sait comment faire en sorte que Jest nous donne cet objet clock ?

Comme d'autres, ma motivation pour utiliser await new Promise(setImmediate); est de vider les promesses résolvables, afin que je puisse tester unitairement leur impact sur le système.

Il semblerait que les fausses minuteries "modernes" sous-performent effectivement les autres en chronométrant apparemment de manière absurde.

Voici quelques tests unitaires pour décrire cela :

describe('flushing of js-queues using different timers', () => {
  beforeAll(() => {
    // It would take the failing test 5 long seconds to time out.
    jest.setTimeout(100);
  });

  it.each([
    [
      'given real timers',
      () => {
        jest.useRealTimers();
      },
    ],
    ['given no timers', () => {}],
    [
      'given "legacy" fake timers',
      () => {
        jest.useFakeTimers('legacy');
      },
    ],
    [
      // This is the the failing scenario, not working like the other timers.
      'given "modern" fake timers',
      () => {
        jest.useFakeTimers('modern');
      },
    ],
  ])(
    '%s, when using setImmediate to flush, flushes a promise without timing out',
    async (_, initializeScenarioSpecificTimers) => {
      initializeScenarioSpecificTimers();

      let promiseIsFlushed = false;

      Promise.resolve().then(() => {
        promiseIsFlushed = true;
      });

      // Flush promises
      await new Promise(setImmediate);

      expect(promiseIsFlushed).toBe(true);
    },
  );
});

J'ai l'impression que le test précédent ne devrait pas échouer comme il le fait.

Pour moi, la solution de contournement consistait à vider les promesses en utilisant le nœud "setImmediate" natif du package "timers", au lieu du global "setImmediate". Ayant cela, les passes suivantes :

import { setImmediate as flushMicroTasks } from 'timers';

it('given "modern" fake timers, when using native timers to flush, flushes a promise without timing out', async () => {
  jest.useFakeTimers('modern');

  let promiseIsFlushed = false;

  Promise.resolve().then(() => {
    promiseIsFlushed = true;
  });

  // Flush micro and macro -tasks
  await new Promise(flushMicroTasks);

  expect(promiseIsFlushed).toBe(true);
});

Merci @aleclarson.

Voici notre solution à ce problème :

https://github.com/team-igniter-from-houston-inc/async-fn
https://medium.com/houston-io/how-to-unit-test-asynchronous-code-for-javascript-in-2020-41c124be2552

Le code de test peut être écrit comme :

// Note: asyncFn(), extends jest.fn() with a way to control resolving/rejecting of a promise
const load = asyncFn();

const afterLoad = jest.fn();
const result = 'mock result';

mount(<Component load={load} afterLoad={afterLoad} />);

// ... some interaction that requires the `load`

// Note: New way to controlling when promise resolves
await load.resolve(result);

expect(afterLoad).toHaveBeenCalledWith(result);

Notez que vous n'avez pas besoin de savoir quoi que ce soit sur le vidage des promesses ou l'exécution des minuteries.

@jansav sympa/+1. Fwiw j'ai vu cette approche appelée le modèle différé. Je pense que cela permet de meilleurs tests.

Il me semble que le problème avec les fausses minuteries est qu'elles interrompent la boucle d'exécution naturelle pour savoir comment les minuteries sont censées fonctionner. Je me demande pourquoi nous ne pouvons pas simplement faire en sorte que les fonctions d'exécution de la minuterie jest soient asynchrones? Changer les minuteurs pour les résoudre de manière synchrone donne au code de test une apparence soignée, mais cela provoque cet effet secondaire massif.

mon cas d'utilisation :

public static resolvingPromise<T>(result: T, delay: number = 5): Promise<T> {
    return new Promise((resolve) => {
        setTimeout(
            () => {
                resolve(result);
            },
            delay
        );
    });
}

fichier d'essai :

it("accepts delay as second parameter", async () => {
    const spy = jest.fn();
    MockMiddleware.resolvingPromise({ mock: true }, 50).then(spy);
    jest.advanceTimersByTime(49);
    expect(spy).not.toHaveBeenCalled();
    jest.advanceTimersByTime(1);
    await Promise.resolve(); // without this line, this test won't pass
    expect(spy).toHaveBeenCalled();
});
Cette page vous a été utile?
0 / 5 - 0 notes