Search code examples
unit-testingjestjses6-promise

How to chain promises with multiple expections in jest


I am trying to find a clean way to write an integration test (unit test with jest which uses real resources) with jest using promise chaining but I can get my head around it.

In addition of actual nesting the promises correctly I also don't know how to solve those two issues:

  • How to have different expects in different promises being picked up correctly by jest
  • having always a teardown where the resources are cleaned up even if there are exceptions

There are 4 function I would like to chain, similar to the Act - Arrange - Assert pattern. The problem is that those funktion have async calls to f.e. a database and can have expectation.

To have a better understanding here is some pseudocode how this could look in my head. This only should provide a better understanding of the issue.

const setupResources =  () =>  new Promise(resolve => {
  //async calls to a database to setup a test
  await dynamoDBClient.put(params).promise();
  // returns data which are necessary for the other steps
  resolve(data);
});

const act = (data) => new Promise(resolve => {
  // doing some stuff
  const otherdata = {data};
    // calling some endpoint asyc
   const result = await axios.post(
              `https://restapi.com/test`,
              JSON.stringify(otherdata),
          );
  // some expects
  expect(result.status).toBe(200);  

  // again some data which will be needed in the other steps
  resolve({someInformation: 'info', data}) ;
});

const assertIt = (data) => {
  const prarams = {/*db related data*/}
  new Promise(resolve=>{
      const dbdata = await dynamoDBClient.get(params).promise();

      // some expectation
      expect(dbdata).toBe(data.data);
      resolve();
 })

};

const tearDown = (data) => {
  // cleanup of data base trash which should always happen
  await dynamoDBClient.delete(data.params).promise();
};

The actual test should look something along those line if that makes sense.

it('test',()=>{
return setupResources()
        .then(act(data))
        .then(assertIt(data))
        .finally(teardown(data));
})

Solution

  • async..await is a substitute for raw promises, there's no need to use then. The use of new Promise with existing promises is an antipattern. One of problems that async function solves is that data that needs to be available down the promise chain doesn't need to be passed through the entire chain, it can be declared as a variable in a scope that is available for the chain.

    It should be:

    const setupResources = async () => {
      await dynamoDBClient.put(params).promise();
      ...
      return data;
    });
    

    And the test:

    it('test', async ()=> {
      let data;
    
      try {
        data = await setupResources();
        ...
       } finally {
         await teardown(data);
       }
    });
    

    Setup and teardown is a task that beforeEach and afterEach blocks solve. If there's a need to extract them to reusable functions, common data can be shared via context object like shown here.