Comment tester les rappels asynchrones XMLHttpRequest dans Jest?

Oct 04 2020

J'ai le test Jest suivant qui comprend l'appel d'un serveur simulé en utilisant ajax avec XMLHttpRequest:

import mock from "xhr-mock";

describe("ajax callbacks", function() {
  beforeEach(function() {
    mock.setup();
  });

  afterAll(function() {
    mock.teardown();
  });

  it("gets called when done", function(done) {
    mock.get("/get-url", {
      status: 200,
      body: '{ "key": "value" }'
    });

    const doneCallback = jest.fn();

    const xhr = new XMLHttpRequest();
    xhr.open("GET", "/get-url");
    xhr.onreadystatechange = function() {
      if (xhr.readyState === XMLHttpRequest.DONE) {
        doneCallback();
        done();
      }
    }
    xhr.send();

    expect(doneCallback).toHaveBeenCalled();
  });
});

ce qui échouera évidemment car l'appel AJAX est traité de manière asynchrone et l'attente est faite avant l'appel du rappel.

Y a-t-il un moyen pour Jest d'attendre que le rappel soit appelé avant de faire l'attente?

Veuillez noter que je ne peux pas transformer la demande en une demande synchrone en raison des exigences du domaine. Et je ne peux pas non plus en faire une API basée sur Promise juste pour pouvoir la tester. Ceci est juste une version simplifiée du test qui est en cours d'écriture afin que les gens ici puissent le saisir facilement. Le code réel est différent et il contient des abstractions à ce qui est écrit ici.

Réponses

1 radiantshaw Oct 06 2020 at 00:47

Je l'ai résolu en utilisant essentiellement le async/awaitsupport fourni par Jest. La solution consiste à encapsuler la demande asynchrone dans un Promiseet à résoudre le Promisemoment où le onreadystatechangerappel est appelé. En tant que tel:

import mock from "xhr-mock";

describe("ajax callbacks", function() {
  beforeEach(function() {
    mock.setup();
  });

  afterAll(function() {
    mock.teardown();
  });

  it("gets called when done", async function() {
    mock.get("/get-url", {
      status: 200,
      body: '{ "key": "value" }'
    });

    const doneCallback = jest.fn();

    const xhr = new XMLHttpRequest();
    xhr.open("GET", "/get-url");

    await new Promise(function(resolve) {
      xhr.onreadystatechange = function() {
        if (xhr.readyState === XMLHttpRequest.DONE) {
          doneCallback();
          resolve();
        }
      }
      xhr.send();
    });

    expect(doneCallback).toHaveBeenCalled();
  });
});

En utilisant await, le test sera suspendu jusqu'à ce que le Promiseproblème soit résolu. Je sais que cela semble un peu piraté. Mais c'est tout ce que nous avons pour le moment. J'ai essayé de chercher d'autres solutions mais je n'en ai trouvé aucune.

Pour en savoir plus sur l'utilisation async/awaitavec Jest, reportez-vous ici .

1 user3700765 Oct 15 2020 at 03:56

Vous pouvez utiliser Sinon pour simuler XMLHttpRequests.

import { createFakeServer } from 'sinon';

describe('Example', () => {

    let xhr;

    beforeEach(() => xhr = createFakeServer());

    afterEach(() => xhr.restore());

    test('example calls callback', () => {
        jest.spyOn(exampleObject, 'exampleCallback');

        xhr.respondWith('POST', '/expected/url', [200, { }, JSON.stringify({ foo: 'response' })]);

        exampleObject.funcToDoRequest();

        xhr.respond();
    
        expect(exampleObject.exampleCallback).toBeCalledWith({ foo: 'response' });
    });

});

Voir plus d'informations sur https://sinonjs.org/releases/v9.2.0/fake-xhr-and-server/

Une autre façon serait d'utiliser setTimeout si xhr-mock est utilisé. Enveloppez l'assertion de rappel à setTimeout et appelez le rappel done () pour le test Jest. C'est une sorte de hack, mais ça marche.

rest of your code
...

setTimeout(() => {
  expect(doneCallback).toHaveBeenCalled();
  done();
});