Jest: Stellen Sie eine API bereit, um die Promise-Auflösungswarteschlange zu leeren

Erstellt am 23. Nov. 2016  ·  46Kommentare  ·  Quelle: facebook/jest

Möchten Sie eine Funktion anfordern oder einen Fehler melden?

_Feature_, denke ich, aber ein ziemlich wichtiger Punkt beim Testen von Code, der Promise s verwendet.

Wie ist das aktuelle Verhalten?

Ich habe eine Komponente, die das Umschließen und Verketten von Promise intern im Anschluss an eine externe asynchrone Aktion verwendet. Ich stelle das Modell der asynchronen Aktion bereit und löse das Versprechen auf, das in meinem Test zurückgegeben wird.

Die Komponente ungefähr so:

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

Und der Testcode sieht ungefähr so ​​aus:

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

Der Test schlägt fehl, weil expect() vor den verketteten Promise-Handlern ausgewertet wird. Ich muss die Länge der inneren Versprechenskette im Test replizieren, um das zu bekommen, was ich brauche, wie folgt:

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

Was ist das erwartete Verhalten?

Ich würde erwarten, dass Jest eine Art API bereitstellt, um alle ausstehenden Promise-Handler zu leeren, z.

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

Ich habe runAllTicks und runAllTimers erfolglos ausprobiert.


_Alternativ, wenn ich nur ein bereits vorhandenes Feature oder Muster vermisse, hoffe ich auf jemanden hier, der mich in die richtige Richtung weist :)_

Enhancement New API proposal

Hilfreichster Kommentar

Eine Hilfsfunktion kann dies selbst in ein Promise umwandeln, sodass Sie sich nicht mit dem erledigten Callback befassen müssen. Es ist klein genug und ziemlich harmlos, um es im Userland zu behalten, aber ich würde mich nicht beschweren, wenn es auf das Scherzobjekt gelegt würde. So etwas wird in meinen Projekten oft verwendet.

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

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

Mit Async-Await ist es fast schon hübsch:

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

Alle 46 Kommentare

Während asynchrones Testen verspricht, ist es gut, daran zu denken, dass Sie eine Testfunktion als Promise zurückgeben können, also funktioniert so etwas wie folgt:

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

Das Zurückgeben eines Promise von der Testfunktion macht Jest bewusst, dass es sich um einen asynchronen Test handelt, und muss warten, bis er behoben ist oder das Zeitlimit überschritten wird.

@thymikee Natürlich .then(() => {}) in Ihrem Code belassen haben. Ich sehe nicht, wie ich das Problem prägnanter beschreiben kann, als ich es bereits im Eingangspost getan habe. Bitte lesen Sie es gründlich durch und öffnen Sie das Problem entweder erneut oder beschreiben Sie, wie Sie es umgehen können.

_Ich habe return zum Code im OP hinzugefügt, um Verwirrung zu vermeiden._

Bin auf ein ähnliches Problem gestoßen und habe es hier beschrieben: https://github.com/pekala/test-problem-example

Kurz gesagt: Ich versuche, eine Sequenz von Aktionen zu bestätigen, die als Ergebnis einer Benutzerinteraktion (mit Hilfe von Enzymen simuliert) an den Redux-Speicher gesendet werden. Die Aktionen als gesendete Synchronisierung und Asynchronisierung mithilfe von Versprechen (verspottet, um sofort aufgelöst zu werden). Es scheint keine Möglichkeit zu geben, eine Aussage zu treffen, nachdem die Versprechenskette erschöpft ist, wenn Sie keinen direkten Zugriff auf die Versprechenskette haben. setTimeout(..., 0) funktioniert, aber es fühlt sich hackig an und wenn die Assertion im Callback von setTimeout fehlschlägt, schlägt Jest mit einem Timeout-Fehler (anstelle eines Assertion-Fehlers) fehl.

Die Idee von flushAllPromises scheint eine Lösung zu sein, obwohl ich denke, dass runAllTicks das tun sollte?

Als Fortsetzung: Ich habe versucht, setTimeout(..., 0) durch setImmediate ersetzen, und dies scheint sowohl die Assertionen auszuführen, nachdem die Promise-Callback-Mikrotask-Warteschlange erschöpft ist, und verhindert, dass Jest bei Assertionsfehlern eine Zeitüberschreitung erzielt. Das funktioniert also in Ordnung und ist eine akzeptable Lösung für meinen Anwendungsfall:

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

Eine Hilfsfunktion kann dies selbst in ein Promise umwandeln, sodass Sie sich nicht mit dem erledigten Callback befassen müssen. Es ist klein genug und ziemlich harmlos, um es im Userland zu behalten, aber ich würde mich nicht beschweren, wenn es auf das Scherzobjekt gelegt würde. So etwas wird in meinen Projekten oft verwendet.

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

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

Mit Async-Await ist es fast schon hübsch:

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

@jwbay das ist doch richtig schön Zucker !

Es ist wahr, dass sich dieses flushPromises Einzeiler herausstellt, aber es ist überhaupt nicht offensichtlich, wie man zu dieser einen Zeile kommt. Daher denke ich, dass es für Jest-Benutzer von Vorteil wäre, es als Util-Funktion zur Verfügung zu haben.

@pekala die

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

Was ist mit dem Versprechen von Swazzling und wenn ein neues Versprechen erstellt wird, fügen Sie es zu einem Array hinzu und spülen Sie alle Versprechen, die auf Promise.all über dieses Array warten?

@talkol Ich denke, das wird es, solange Sie auch die gefälschten Timer verwenden. Getestet habe ich das aber nicht.

@pekala keine Notwendigkeit, Timer mit diesem Beispiel zu fälschen, da das Versprechen erst aufgelöst wird, wenn die Zeit erreicht ist
Ich mache mir nur Sorgen, dass das knisternde Promise das scherzhafte Innenleben durcheinander bringt, es ist ein bisschen harter Kern

Wenn Sie keine Timer fälschen, werden Ihre Tests echte 2 Sekunden dauern. Ich denke, die beste Vorgehensweise wäre, diese Art von Verzögerungen zu entfernen. In diesem Fall erledigt das von @jwbay vorgeschlagene flushPromises die Aufgabe.

Alles hängt davon ab, was Sie testen möchten :) Alles, was ich sage, ist, dass die Timer ein unabhängiges Anliegen sind, um auf die Versprechen zu warten

Wir stoßen auf Probleme im Zusammenhang mit nicht aufgelösten Versprechen, die mit setTimeout-Aufrufen vermischt sind. In jest v19.0.2 haben wir keine Probleme, aber in jest v20.0.0 gehen Promises nie in die Funktionen zum Auflösen/Ablehnen und daher schlagen Tests fehl. Unser Problem scheint damit zusammenzuhängen, dass wir keine _API zum Leeren der Promise-Auflösungswarteschlange haben_, aber dieses Problem scheint vor jest v20.0.0 zu liegen, wo wir das Problem zu sehen begannen, daher bin ich mir nicht ganz sicher.

Dies ist die einzige Lösung, die wir für einige unserer Tests finden konnten, da wir eine Reihe von abwechselnden setTimeout s und Promise s haben, die in dem Code verwendet werden, der schließlich das aufruft onUpdateFailed Rückruf.

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

Nicht so hübsch, daher ist jeder Rat hier sehr willkommen.

Ein weiteres Beispiel, bei dem Sie das Versprechen vom Test nicht zurückgeben können:

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

Dieser Test schlägt mit jest 20.0.4 fehl.

Die Lösung von @philwhln kann auch mit async/await geschrieben werden

ReactTestUtils.Simulate.submit(form);

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

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

Ich würde mich über eine Utility-Funktion freuen, die die Promise-Warteschlange leert

Ich würde mich auch über eine Funktion freuen, die die Promise-Warteschlangen zwischen den Tests leert.

Ich teste Code, der Promise.all verwendet, um mehrere Versprechen zu umschließen. Wenn eines dieser umschlossenen Promises fehlschlägt (weil ich das testen möchte), kehrt das Promise sofort zurück, was bedeutet, dass die anderen Promises manchmal (Race Condition, nicht deterministisch) zurückkehren, während der nächste Test läuft.

Dies verursacht alle möglichen Verwüstungen, da meine Tests nicht vorhersehbare / wiederholbare Ergebnisse haben.

Um dies richtig zu implementieren, müssten wir Promise nachahmen, damit wir schließlich alle in die Warteschlange gestellten Mikroaufgaben sehen können, um sie synchron zu lösen. Etwas in der Art, was Versprechen-Mock macht.

Es gibt bereits eine API, um die mit process.nextTick in die Warteschlange gestellten Mikroaufgaben zu leeren, und diese API sollte wahrscheinlich auch mit Promises ( jest.runAllTicks ) funktionieren.

Ich hatte eine Lösung mit Jasmin, die sich in den nextTick von Yaku, eine Versprechensbibliothek, einhakte und nextTick-Anrufe auffing und sie frühzeitig abspielen ließ.
Allerdings verspricht sich Scherz, was dies problematisch machte.
Am Ende habe ich Yaku genommen und es gehackt, um eine Flush-Methode zu haben, die seine Warteschlange ausspült. Standardmäßig läuft es normal mit nextTick, aber wenn Sie flush aufrufen, werden alle ausstehenden Promise-Handler ausgeführt.
Die Quelle ist hier:
https://github.com/lukeapage/yaku-mock
Es könnte mit Aufräumen, Kontaktaufnahme mit ysmood, um zu sehen, was sie davon halten und Dokumentation hinzufügen, aber es tut so ziemlich das, was Sie wollen und funktionierte für mich als einfache Lösung, um Versprechen in Tests zu synchronisieren.

Als einfachen Workaround gefällt mir die Lösung von

Wie wäre es, wenn wir etwas Ähnliches wie das Objekt jest hinzufügen?

await jest.nextTick();

Implementiert als

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

cc @cpojer @SimenB @rogeliog

Ich verwende Enzyme, um React-Komponenten zu montieren.

Auch ich habe Funktionen, die die Ausführung von Promises erwarten, aber keine der oben genannten Fixes hat funktioniert. Ich könnte sie in meinem Test synchron behandeln - wenn - die Funktionen die Promise-Objekte mit await , aber leider geben die Funktionen die Promise-Objekte nicht zurück.

Dies ist die Problemumgehung, die ich mit einem Spion für die globale Promise-Funktion durchgeführt habe.

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

Ich bin auf einen Anwendungsfall dafür gestoßen (danke

Sie möchten beispielsweise überprüfen, ob Ihre Funktion eine Zeitüberschreitung hat und dass die Zeitüberschreitung genau erzwungen wird:

      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

Die Rückgabe einer Zusage erlaubt es nicht, den genauen Zeitpunkt der Auflösung/Ablehnung zu überprüfen.

Da ist eine Versprechensspülung erforderlich. Ohne sie wird die Erwartung zu früh aufgerufen.

Hoffe das hilft das Problem einzugrenzen.

Für Mitläufer gibt es dazu hier eine offene PR: #6876

Crossposting von https://github.com/airbnb/enzyme/issues/1587

Ich frage mich, ob das folgende Muster ausreichen sollte, um dieses Problem zu lösen, und ob ich etwas tue, das als schlechte Praxis angesehen wird und ich es nicht tun sollte.

Was halten die Leute von diesem Ansatz?

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
}

Und in den 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)
})

Ich hatte ein Problem, als der asynchrone Aufruf nicht direkt in ComponentDidMount ausgeführt wurde, sondern eine asynchrone Funktion aufrief, die eine andere asynchrone Funktion aufrief und so weiter. Wenn ich in der gesamten asynchronen Kette einen zusätzlichen asynchronen Schritt hinzugefügt habe, müsste ich ein zusätzliches .then() oder ein zusätzliches await hinzufügen, aber das funktioniert einwandfrei.

Gibt es einen Grund, warum ich diesen Ansatz nicht verwenden sollte oder sieht das für die Leute gut aus?

Ich habe ein Abenteuer damit im Userland gemacht und festgestellt, dass es tatsächlich machbar und nicht so schlimm ist (obwohl es einige Fallstricke gibt, in die man stolpern muss, wenn man keine Karte hat). Hier ist ein Erfahrungsbericht, der (hoffentlich) detailliert genug ist, um ihn direkt verwenden zu können ; ein TLDR besteht darin, async / await in Promises zu transpilieren und native Promises gegen Bluebird und native Timer gegen Lolex auszutauschen; transpiliere alles , einschließlich node_modules/ ; queueMicrotask ist das Primitiv, das Sie für Versprechen benötigen, aber standardmäßig wird es von lolex nicht bereitgestellt, da JSDOM es nicht bereitstellt.

Ich bin auf das gleiche Problem mit jest.mockAllTimers() und React-Komponenten gestoßen, die ein Promise in componentDidMount() aufrufen.

Die Lösung von #issuecomment-279171856 hat das Problem auf elegante Weise gelöst.

Wir brauchen etwas Ähnliches in der offiziellen Jest-API!

Ich bin erst vor kurzem auf ein Problem gestoßen, als ich eine Reihe von Sachen aktualisiert habe. Es hat ein Problem in einer Reihe von Tests aufgedeckt, bei dem wir nicht immer darauf warteten, dass die Versprechen abgeschlossen wurden. Und während Methoden wie await new Promise(resolve => setImmediate(resolve)); in einfachen Fällen funktionierten, stellte ich in meinen Tests fest, dass ich das ein paar Mal ausführen musste, um die Pipe zu leeren. Das hat @quasicomputational in ihrer Untersuchung hier erwähnt . Leider glaube ich nicht, dass es eine Möglichkeit gibt, zu wissen, wann diese Pipe frei ist, ohne die Versprechen abzufangen, wenn sie erstellt werden. Also habe ich eine kleine Bibliothek erstellt zu tun , dass ... Versprechen-Spion . Ich hatte jedoch einen Test, bei dem gefälschte Timer verwendet wurden, und er funktionierte damit nicht ... also ist es noch keine vollständig funktionierende Lösung.

Obwohl ich mir auch vorstellen kann, dass sie nur mit async / await in Ihrem zu testenden Code funktionieren, wenn sie in Versprechen umgewandelt werden. Wenn sie nicht in Versprechen umgewandelt werden, kann sich diese Bibliothek nicht in sie einklinken und warten, bis sie abgeschlossen sind.

Ich hatte das gleiche Problem und stellte fest:
Wir sollten ausstehende Versprechen nicht leeren, sondern den gesamten Test fehlschlagen lassen, wenn ausstehende Versprechen vorhanden sind.
Auf diese Weise werden wir gezwungen, ausstehende Versprechen innerhalb des getesteten Codes mit dem Abort Controller abzubrechen:
https://developers.google.com/web/updates/2017/09/abortable-fetch
Scherzhafte Versprechungen zu haben, ist gleichbedeutend mit der Aussage "Gleichzeitigkeit ist schwer, also lass es uns nicht testen". In Wirklichkeit sollte es genau das Gegenteil sein.
Da Parallelität schwer ist, sollten wir sie noch mehr testen und überhaupt keinen Test mit ausstehenden Versprechen passieren lassen.

Angesichts des Chaos beim Abbruch von Versprechen in dieser Stackoverflow-Frage ist klar, dass es (NOCH) nicht einfach ist:
https://stackoverflow.com/a/53933849/373542
Ich werde versuchen, eine KISS-Implementierung zum Abbrechen meiner Abrufversprechen zu schreiben und das Ergebnis hier posten.

@giorgio-zamparelli: _"Gleichzeitigkeit ist schwer, also testen wir sie nicht"_ ist völlig neben dem Punkt des ursprünglichen Berichts. Das Problem betrifft nicht _ausstehende_ Promises, sondern vielmehr die Tatsache, dass das Warten auf die Weitergabe der Promise-_Resolution_ durch asynchronen Code in Tests unnötig schwierig ist.

Ich denke, Versprechen zu spülen, würde die Symptome heilen, anstatt die Krankheit.

Versprechen sollten sich in Tests normal auflösen, ohne dass eine Spülung erforderlich ist.
Wenn es in Ihrem Test eine ausstehende Zusage gibt, sollten Sie entweder warten, bis sie aufgelöst ist, indem Sie beispielsweise wait von @testing-library/react ODER wenn die ausstehende Zusage nicht Teil des Testumfangs ist, sollten Sie entweder Verspotten Sie den Code, um ihn zu starten, oder Sie sollten das ausstehende Versprechen irgendwo wie beim React willUnmount-Lebenszyklusereignis mit dem AbortController abbrechen

Der AbortController ist eine neue API, die fast niemand verwendet, und ich habe das Gefühl, dass die meisten der hängenden Versprechen in Tests behoben sind.

WIDERLEGE MICH:
Mir könnte leicht das Gegenteil bewiesen werden, wenn jemand, der berichtet hat, dass er Probleme mit anstehenden Problemen in dieser Ausgabe hat, bereits versucht hat, AbortController und jest.mock zu verwenden, und es war nicht genug.

@giorgio-zamparelli: Vielleicht stammt das Missverständnis von meiner Verwendung des Ausdrucks _"Alle ausstehenden Versprechenshandler leeren"_ (und wenn ja, tut es mir leid). Wie Sie hoffentlich sehen werden, wenn Sie die Problembeschreibung gründlich lesen, meinte ich "ausstehende Versprecher".

Um es noch einmal zu wiederholen, wir sprechen hier (in keiner Weise) über _ausstehende_ Versprechen, sondern eher über das Löschen von Versprechensauflösungen mit minimalem Aufwand. Oder mit anderen Worten, transparent und deterministisch von der Auflösung eines Versprechens zu dem Punkt zu gelangen, an dem alle damit verbundenen Folgewirkungen aufgerufen werden (um das Ergebnis zu testen).

Zu diesem Zweck habe ich kürzlich flush-microtasks . Es leiht seine Umsetzung von Reaktion, die als @jwbay erstaunlich komplexen ‚s Lösung hier oder @thymikee‘ s Lösung hier . Ich bin mir nicht sicher, ob die Komplexität einen sinnvollen Unterschied macht, aber ich gehe davon aus, dass sie Randfälle berücksichtigt, die von den anderen Lösungen in diesem Thread nicht berücksichtigt werden. Ich habe diese Implementierung nur verwendet, weil react-testing-library verwendet (siehe hier ), sie jedoch nicht verfügbar macht.

import { flushMicroTasks } from 'flush-microtasks'

await flushMicroTasks()

@aleclarson Gibt es einen Unterschied zwischen Flush-Microtasks und Flush-Promises ?

@ramusus Sieht aus wie flush-promises verwendet den gleichen Ansatz wie die Lösung von @jwbay .

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

RTL hat auch den Code von React kopiert: https://github.com/testing-library/react-testing-library/blob/8db62fee6303d16e0d5c933ec1fab5841dd2109b/src/flush-microtasks.js

EDIT: hah, schon erwähnt :grins:

Ich bin mir nicht sicher, ob wir es in Jest einbauen müssen, wenn die Leute das nutzen können? Vielleicht können wir es in den Dokumenten verlinken? Bei diesem Problem geht es darum, sie synchron zu leeren, was meiner Meinung nach über das hinausgeht, was wir tun möchten (vor allem, da dies mit async-await unmöglich ist).

Die flushPromises Lösung funktioniert nur bei Versprechen, die sofort aufgelöst werden, aber nicht bei denen, die noch ausstehen.

Hm, guter Punkt. Ich weiß nicht, ob es möglich ist, pending Versprechen irgendwie zu verfolgen. Könnte mit async_hooks etwas cleveres machen, bin mir nicht sicher. Es wird wahrscheinlich mühsam, zwischen Versprechen, die von Userland-Code erstellt wurden, und Versprechen, die von Jest und seinen Abhängigkeiten erstellt wurden, zu unterscheiden

Ich habe versucht, das Promise Objekt zu umschließen/zu verspotten, um einen Zähler einzuschließen, aber das funktioniert nicht:

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

Hauptproblem sind async Funktionen, die die globalen Promise überhaupt nicht verwenden

Ok, ich habe einen wirklich hackigen Weg wie diesen gefunden .

  1. Erstellen Sie ein neues Modul mit einer Liste.
  2. Fügen Sie Ihre Versprechen zu dieser Liste hinzu.
  3. Lösen Sie die Versprechen in Ihrem Test und entfernen Sie sie aus der Liste.
  4. In meinem Fall führe ich wrapper.update() von Enzym aus. Machen Sie hier ggf. etwas Ähnliches.
  5. Wiederholen Sie Schritt 3 und 4, bis die Liste leer ist.

Ich weiß, es ist keine gute Praxis, Code an die Tests anzupassen, ABER ich verwende diese Logik bereits beim serverseitigen Rendering. Aber am Ende wartet es nur. ¯\_(ツ)_/¯

Dazu gibt es ein interessantes Update in Jest 26, wo gefälschte Timer jetzt auf @sinon/fake-timers basieren (sofern mit jest.useFakeTimers('modern') aktiviert).

Ich habe die modernen gefälschten Timer mit meinen Tests ausprobiert, und leider führt dies dazu, dass der await new Promise(resolve => setImmediate(resolve)); Hack auf unbestimmte Zeit hängen bleibt. Glücklicherweise enthält @sinon/fake-timers mehrere *Async() Methoden, die "auch die Ereignisschleife unterbrechen, sodass alle geplanten Promise-Callbacks ausgeführt werden können, _bevor_ die Timer ausgeführt werden.". Leider sehe ich keine Möglichkeit, das clock Objekt über die Jest-APIs abzurufen.

Weiß jemand, wie man Jest dazu bringt, uns dieses clock Objekt zu geben?

Wie bei anderen ist meine Motivation, await new Promise(setImmediate); zu verwenden, um auflösbare Versprechen zu leeren, damit ich ihre Auswirkungen auf das System testen kann.

Es scheint, dass die "modernen" gefälschten Timer andere in der Tat unterbieten, indem sie scheinbar unsinniges Zeitlimit überschreiten.

Hier sind einige Unit-Tests, um dies zu beschreiben:

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

Ich habe das Gefühl, dass der vorherige Test nicht so scheitern sollte.

Für mich bestand die Problemumgehung darin, Versprechen zu leeren, indem das knotennative "setImmediate" aus dem Paket "timers" anstelle des globalen "setImmediate" verwendet wurde. Damit passiert Folgendes:

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

Danke @aleclarson.

Hier ist unsere Lösung für dieses Problem:

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

Testcode kann wie folgt geschrieben werden:

// 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);

Beachten Sie, dass Sie nichts über das Löschen von Versprechen oder das Ausführen von Timern wissen müssen.

@jansav nett/+1. Fwiw Ich habe diesen Ansatz gesehen, der als verzögertes Muster bezeichnet wird. Ich denke, es macht schönere Tests.

Es scheint mir, dass das Problem mit gefälschten Timern darin besteht, dass sie die natürliche Ablaufschleife für die Funktionsweise von Timern unterbrechen. Ich frage mich, warum wir die Lauffunktionen des Jest-Timers nicht einfach asynchron haben können. Das Ändern von Timern zur synchronen Auflösung lässt den Testcode ordentlich aussehen, aber es verursacht diesen massiven Nebeneffekt.

mein Anwendungsfall:

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

Testdatei:

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();
});
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen