4

I use Supertest to test my Express apps, but I'm running into a challenge when I want my handlers to do asynchronous processing after a request is sent. Take this code, for example:

const request = require('supertest');
const express = require('express');

const app = express();

app.get('/user', async (req, res) => {
  res.status(200).json({ success: true });
  await someAsyncTaskThatHappensAfterTheResponse();
});

describe('A Simple Test', () => {
  it('should get a valid response', () => {
    return request(app)
      .get('/user')
      .expect(200)
      .then(response => {
          // Test stuff here.
      });
  });
});

If the someAsyncTaskThatHappensAfterTheResponse() call throws an error, then the test here is subject to a race condition where it may or may not failed based on that error. Even aside from error handling, it's also difficult to check for side effects if they happen after the response is set. Imagine that you wanted to trigger database updates after sending a response. You wouldn't be able to tell from your test when you should expect that the updates have completely. Is there any way to use Supertest to wait until the handler function has finished executing?

Ivanna
  • 1,197
  • 1
  • 12
  • 22

2 Answers2

0

This can not be done easily because supertest acts like a client and you do not have access to the actual req/res objects in express (see https://stackoverflow.com/a/26811414/387094).

As a complete hacky workaround, here is what worked for me.

Create a file which house a callback/promise. For instance, my file test-hack.js looks like so:

let callback = null
export const callbackPromise = () => new Promise((resolve) => {
  callback = resolve
})
export default function callWhenComplete () {
  if (callback) callback('hack complete')
}

When all processing is complete, call the callback callWhenComplete function. For instance, my middleware looks like so.

import callWhenComplete from './test-hack'

export default function middlewareIpnMyo () {
  return async function route (req, res, next) {
    res.status(200)
    res.send()

    // async logic logic
    callWhenComplete()
  }
}

And finally in your test, await for the callbackPromise like so:

import { callbackPromise } from 'test-hack'

  describe('POST /someHack', () => {
    it.only('should handle a post request', async () => {

      const response = await request
        .post('/someHack')
        .send({soMuch: 'hackery'})
        .expect(200)

      const result = await callbackPromise()

      // anything below this is executed after callWhenComplete() is 
      // executed from the route

    })
})

Travis Stevens
  • 2,198
  • 2
  • 17
  • 25
0

Inspired by @travis-stevens, here is a slightly different solution that uses setInterval so you can be sure the promise is set up before you make your supertest call. This also allows tracking requests by id in case you want to use the library for many tests without collisions.

const backgroundResult = {};

export function backgroundListener(id, ms = 1000) {
  backgroundResult[id] = false;
  return new Promise(resolve => {
    // set up interval
    const interval = setInterval(isComplete, ms);
    // completion logic
    function isComplete() {
      if (false !== backgroundResult[id]) {
        resolve(backgroundResult[id]);
        delete backgroundResult[id];
        clearInterval(interval);
      }
    }
  });
}

export function backgroundComplete(id, result = true) {
  if (id in backgroundResult) {
    backgroundResult[id] = result;
  }
}

Make a call to get the listener promise BEFORE your supertest.request() call (in this case, using agent).

  it('should respond with a 200 but background error for failed async', async function() {
    const agent = supertest.agent(app);
    const trackingId = 'jds934894d34kdkd';
    const bgListener = background.backgroundListener(trackingId);

    // post something but include tracking id
    await agent
      .post('/v1/user')
      .field('testTrackingId', trackingId)
      .field('name', 'Bob Smith')
      .expect(200);

    // execute the promise which waits for the completion function to run
    const backgroundError = await bgListener;
    // should have received an error
    assert.equal(backgroundError instanceof Error, true);
  });

Your controller should expect the tracking id and pass it to the complete function at the end of controller backgrounded processing. Passing an error as the second value is one way to check the result later, but you can just pass false or whatever you like.

// if background task(s) were successful, promise in test will return true
backgroundComplete(testTrackingId);

// if not successful, promise in test will return this error object
backgroundComplete(testTrackingId, new Error('Failed'));

If anyone has any comments or improvements, that would be appreciated :)

MrMaz
  • 31
  • 4