Como testar callbacks XMLHttpRequest assíncronos no Jest?

Oct 04 2020

Eu tenho o seguinte teste Jest, que inclui chamar um servidor simulado usando ajax com 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();
  });
});

que obviamente falhará porque a chamada AJAX é tratada de forma assíncrona e a expectativa é feita antes que o retorno de chamada seja chamado.

Existe alguma maneira em que Jest possa esperar até que o retorno de chamada seja chamado antes de fazer a expectativa?

Observe que não posso transformar a solicitação em uma solicitação síncrona devido aos requisitos de domínio. E também não posso transformá-lo em uma API baseada em Promise apenas para ser capaz de testá-lo. Esta é apenas uma versão simplificada do teste que está sendo escrito para que o pessoal aqui possa compreendê-lo facilmente. O código real é diferente e tem abstrações do que está escrito aqui.

Respostas

1 radiantshaw Oct 06 2020 at 00:47

Resolvi basicamente usando o async/awaitsuporte que Jest oferece. A solução é envolver a solicitação assíncrona em um Promisee resolver o Promisequando o onreadystatechangeretorno de chamada for chamado. Assim sendo:

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

Usar awaitfará o teste pausar até que o problema Promiseseja resolvido. Eu sei que isso parece um pouco estranho. Mas isso é tudo que temos por agora. Tentei procurar outras soluções, mas não consegui encontrar nenhuma.

Para saber mais sobre como usar async/awaitcom Jest, consulte aqui .

1 user3700765 Oct 15 2020 at 03:56

Você pode usar o Sinon para simular 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' });
    });

});

Veja mais informações de https://sinonjs.org/releases/v9.2.0/fake-xhr-and-server/

Outra maneira seria usar setTimeout se xhr-mock for usado. Envolva a asserção de retorno de chamada para setTimeout e chame o retorno de chamada done () para o teste de Jest. Parece uma espécie de hack, mas funciona.

rest of your code
...

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