Jest can’t test an awaited promise, it times out instead

I switched from just running an axios GET, to returning a promise, and now my Jest test is failing:

Downloading the zip in ‘resource.js’:

async function downloadMtgJsonZip() {
  const path = Path.resolve(__dirname, 'resources', fileName);
  const writer = Fs.createWriteStream(path);

  console.info('...connecting...');
  const { data, headers } = await axios({
    url,
    method: 'GET',
    responseType: 'stream',
  });
  return new Promise((resolve, reject) => {
    let error = null;
    const totalLength = headers['content-length'];
    const progressBar = getProgressBar(totalLength);
    console.info('...starting download...');
    data.on('data', (chunk) => progressBar.tick(chunk.length));
    data.pipe(writer);
    writer.on('error', (err) => {
      error = err;
      writer.close();
      reject(err);
    });
    writer.on('close', () => {
      const now = new Date();
      console.info(`Completed in ${(now.getTime() - progressBar.start) / 1000} seconds`);
      if (!error) resolve(true);
      // no need to call the reject here, as it will have been called in the
      // 'error' stream;
    });
  });
}

Neither of the following tests in ‘resource.spec.js’ pass now:

it('fetches successfully data from an URL', async () => {
    const onFn = jest.fn();
    const data = { status: 200, data: { pipe: () => 'data', on: onFn }, headers: { 'content-length': 100 } };

    const writerOnFn = jest.fn();

    axios.mockImplementationOnce(() => data);
    fs.createWriteStream.mockImplementationOnce(() => ({ on: writerOnFn }));
    await downloadMtgJsonZip();
    expect(onFn).toHaveBeenCalledWith('data', expect.any(Function));
    expect(axios).toHaveBeenCalledWith(
      expect.objectContaining({ url: 'https://mtgjson.com/api/v5/AllPrintings.json.zip' }),
    );
    expect(axios).toHaveBeenCalledWith(
      expect.objectContaining({ responseType: 'stream' }),
    );
  });
  it('ticks up the progress bar', async () => {
    const tickFn = jest.fn();
    const dataOnFn = jest.fn((name, func) => func(['chunk']));
    const data = { status: 200, data: { pipe: () => 'data', on: dataOnFn }, headers: { 'content-length': 1 } };

    const writerOnFn = jest.fn();

    ProgressBar.mockImplementationOnce(() => ({ tick: tickFn }));
    axios.mockImplementationOnce(() => data);
    fs.createWriteStream.mockImplementationOnce(() => ({ on: writerOnFn }));
    await downloadMtgJsonZip();

    expect(ProgressBar).toHaveBeenCalledWith(
      expect.stringContaining('downloading'),
      expect.objectContaining({
        total: 1,
      }),
    );
    expect(tickFn).toHaveBeenCalledWith(1);
  });
});

Of note, VSCode is telling me that for axios in ‘resource.js’ ‘this expression is not callable’ and that nothing has mockImplementationOnce (it ‘does not exist on type…’).

Previously my downloadMtgJsonZip looked like this:

async function downloadMtgJsonZip() {
  const path = Path.resolve(__dirname, 'resources', 'AllPrintings.json.zip');
  const writer = Fs.createWriteStream(path);

  console.info('...connecting...');
  const { data, headers } = await axios({
    url,
    method: 'GET',
    responseType: 'stream',
  });
  const totalLength = headers['content-length'];
  const progressBar = getProgressBar(totalLength);
  const timer = setInterval(() => {
    if (progressBar.complete) {
      const now = new Date();
      console.info(`Completed in ${(now.getTime() - progressBar.start) / 1000} seconds`);
      clearInterval(timer);
    }
  }, 100);
  console.info('...starting download...');
  data.on('data', (chunk) => progressBar.tick(chunk.length));
  data.pipe(writer);
}

and the only line that is different in the test is the mock for createWriteStream was simpler (it read fs.createWriteStream.mockImplementationOnce(() => 'fs');)

I’ve tried adding:

  afterEach(() => { 
    jest.clearAllMocks(); 
    jest.resetAllMocks();
  });

I’ve tried adding in writerOnFn('close'); to try and get the writer.on('close', ...) to trigger.

But I am till getting this error:

: Timeout – Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout – Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:

I can’t figure out what is missing, to make the async call to be ‘invoked’. The last time I had this issue mocking out createWriteStream fixed my issue, but I don’t see anything else to mock out?

How do I get these tests to pass again?

Answer

How do the event handlers attached using writer.on(event, handler) get invoked in the test code? Wouldn’t the writerOnFn mock need to call the handler function being passed in? If those aren’t being invoked, then resolve(true) is never being called and so the call to await downloadMtgJsonZip(); inside the test never resolves.

I would think you’d need something like this

const writerOnFn = jest.fn((e, cb) => if (e === 'close') cb())

Of course, you may want to flesh it out to differentiate between the ‘error’ and ‘close’ events, or just make sure to alter it if you have tests around the ‘error’ condition.