Search code examples
javascriptforeachtry-catchsinonsinon-chai

Asserting on catch block code inside forEach loop


I am hard time writing test to assert something happened inside catch block which is executed inside forEach loop.

Prod code

function doSomething(givenResourceMap) {
  givenResourceMap.forEach(async (resourceUrl) => {
    try {
        await axios.delete(resourceUrl);
    } catch (error) {
        logger.error(`Error on deleting resource ${resourceUrl}`);
        logger.error(error);
        throw error;
    }
});

I am wanting to assert logger.error is being called twice and called with right arguments each time. So I wrote some test like this

describe('Do Something', () => {
  it('should log message if fail to delete the resource', function() {        
    const resource1Url = chance.url();
    const givenResourceMap = new Map();
    const thrownError = new Error('Failed to delete');
    givenResourceMap.set(resource1Url);

    sinon.stub(logger, 'error');
    sinon.stub(axios, 'delete').withArgs(resource1Url).rejects(thrownError);

    await doSomething(givenResourceMap);

    expect(logger.error).to.have.callCount(2);
    expect(logger.error.getCall(0).args[0]).to.equal(`Error deleting resource ${resource1Url}`);
    expect(logger.error.getCall(1).args[0]).to.equal(thrownError);
    // Also need to know how to assert about `throw error;` line
  });
});

I am using Mocha, sinon-chai, expect tests. Above test is failing saying logger.error is being 0 times.

Thanks.


Solution

  • The problem is that you are using await on a function that doesn't return a Promise. Note that doSomething is not async and does not return a Promise object.

    The forEach function is async but that means they'll return right away with an unresolved Promise and you don't ever await on them.

    In reality, doSomething will return before the work inside of the forEach is complete, which is probably not what you intended. To do that you could use a regular for-loop like this:

    async function doSomething(givenResourceMap) {
      for (const resourceUrl of givenResourceMap) {
        try {
            await axios.delete(resourceUrl);
        } catch (error) {
            logger.error(`Error on deleting resource ${resourceUrl}`);
            logger.error(error);
            throw error;
        }
      }
    }
    

    Note that it changes the return type of doSomething to be a Promise object rather than just returning undefined as it originally did. But it does let you do an await on it as you want to in the test (and presumably in production code also).

    However since you re-throw the exception caught in the loop, your test will exit abnormally. The test code would have to also change to catch the expected error:

    it('should log message if fail to delete the resource', function(done) { 
      // ... the setup stuff you had before...       
      await doSomething(givenResourceMap).catch(err => {
        expect(logger.error).to.have.callCount(2);
        expect(logger.error.getCall(0).args[0]).to.equal(`Error deleting resource ${resource1Url}`);
        expect(logger.error.getCall(1).args[0]).to.equal(thrownError);
        done();
      });
    });