-1

I am trying to mock the AWS SDK. I have a function, ingest that does some logic (including some calls to the AWS SDK sqs.deleteMessage function) and then calls ingestAndSave (which also makes some calls to the sqs.deleteMessage function).

  • When testing the ingest function, I want to mock my ingestAndSave function so I can have those tests focus purely on the ingest function logic.
  • Then I want to test the ingestAndSave function logic in its own describe block - and test the real thing, not the mocked version.
  • When testing either ingest or ingestAndSave functions I need to use my mocked sqs.deleteMessage

My most recent attempt includes following the path here: https://www.lucasamos.dev/articles/jestmocking. My test suite looks like this:

const ingest = require('./ingest')
const TEST_CONSTANTS = require('../../../tests/constants')

const mockdeleteMessage = jest.fn().mockImplementation( ()=> { 
    console.log("MOCK DELETE MESSAGE FUNCTION");
    return {}
} )

jest.mock('aws-sdk', () => {
    return {
            config: {
                update() {
                    return {}
                }
            },
            SQS: jest.fn(() => {
            return {
                    deleteMessage: jest.fn(( { params } )=> {
                        return {
                            promise: () => mockdeleteMessage()
                        }
                    }),
                  }
            })
    }
});


describe("FUNCTION: ingest", () => {
    let ingestAndSaveFunctionSpy
    beforeEach(()=> {
        ingestAndSaveFunctionSpy = jest.spyOn(ingest, 'ingestAndSave').mockReturnValue(
        Promise.resolve() )
    })

    test("ingestAndSave will be called two times when event has two Records", async () => {
        await ingest.ingest(TEST_CONSTANTS.sqsIngestEventMessageWithTwoRecords)
        expect(ingestAndSaveFunctionSpy).toHaveBeenCalledTimes(2) 
    })
    
    afterEach(() => {
        jest.resetAllMocks()
    })
    afterAll(() => {
        jest.restoreAllMocks()
    })
    
})


describe("FUNCTION: ingestAndSave", () => {
    let validParams = {camera: TEST_CONSTANTS.validCameraObject, sourceQueueURL:"httpexamplecom", receiptHandle: "1234abcd"}

    test("Will call deleteMessage once when given valid paramters", async () => {
        await ingest.ingestAndSave(validParams)
        expect(mockdeleteMessage.mock.calls.length).toBe(1)
    })
    /** snip other tests */
})

Both of the above functions run through code that looks just like this:

let deleteResp;
try {
    deleteResp = await sqs.deleteMessage({ QueueUrl: sourceQueueURL, ReceiptHandle: receiptHandle}).promise()   
} catch (err) {
    console.error('Task: Deleting message from the ingest queue: ', err, 'deleteResp: ', deleteResp, 'receiptHandle: ', receiptHandle)
}

The mocked sqs.deleteMessage is used for the first describe block (i.e. I see the console.log for it) and the test passes.

However, the mocked sqs.deleteMessage function is not used for the second describe block (i.e. I do not see the console.log message indicating the mocked function ran, and, in fact, I get a 5000ms timeout, indicating the real sqs.deleteMessage was called (with invalid authorization, the deleteMessage command takes >5 seconds).

  1. I thought the jest.restoreAllMocks() in the afterAll block of the first describe is restoring my mock. So I go with explicitly restoring the ingestAndSaveFunctionSpy mock with ingestAndSaveFunctionSpy.mockRestore() instead. Unfortunately, this results in the same behavior: the mocked AWS SDK is used in the first describe block, but it has been restored by the ingestAndSaveFunctionSpy.mockRestore() call.

  2. Just to test, I remove the afterAll in the first describe entirely. This results in the second test calling the mocked implementation of ingestAndSave and thus the test failing.

  3. Declare the jest.mock... within each describe block, but this isn't allowed due to jest.mock calls getting hoisted to the top of the file.

How can I mock a module using jest.mock(... and have it persist between describe blocks while allowing mockRestore() calls to other mocked functions?

How to change mock implementation on a per single test basis? has me looking at mockImplemention but I'm struggling to see how I'd implement it.

See related question attempting to tackle this from a different angle: How to have jest.mock persist between tests?

lowcrawler
  • 6,777
  • 9
  • 37
  • 79

2 Answers2

1

If you only want to reset the one mock, then don't reset all of the mocks. Just reset the spy:

describe("FUNCTION: ingest", () => {
    let ingestAndSaveFunctionSpy

    beforeEach(()=> {
        ingestAndSaveFunctionSpy = jest.spyOn(ingest, 'ingestAndSave').mockReturnValue(
        Promise.resolve() )
    })

    // ...
    
    afterEach(() => {
        ingestAndSaveFunctionSpy.mockRestore()
    })
})

In the outer scope, I would also advise using a:

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

This will reset the recordings of the mock calls to your aws-sdk mock without actually changing any of the mocked behavior. Otherwise, you might be verifying behavior in one test that actually happened during the execution of another.


Without seeing the code, it's hard to offer more specific advice, but this style of test is a code smell, IMO. Any time you have to spy on your own unit in a unit test, it's usually a sign of poor organization. Try to arrange it so that discreet units of work are discreet functions. As an abstract example, instead of chaining functions together like:

const a = (arg) => {
  const stuff = /* do some stuff with arg */
  b(stuff);
};

const b = (arg) => {
  const stuff = /* do some stuff with arg */
  c(stuff);
}

const c = (arg) => {
  /* do some stuff with arg */
}

Instead make them discreet, testable units that are joined together in the external function:

const abc = (arg) => {
  const firstStuff = a(arg);
  const secondStuff = b(firstStuff);
  c(secondStuff);
}

const a = (arg) => {
  return /* do some stuff with arg */
}

const b = (arg) => {
  return /* do some stuff with arg */
}

const c = (arg) => {
  /* do some stuff with arg */
}

Now you can write tests for a, b, and c independently without having to mock your own internals.

Eric Haynes
  • 5,126
  • 2
  • 29
  • 36
  • Thanks. Note item #1 above -- running a `mockRestore()` on a specific spy (instead of `restoreAllMocks()`), 'restores' the jest.mock'd aws-sdk and suddenly the real functions are being called again. Perhaps this is a bug in jest and I should make an issue on their repo? – lowcrawler Jun 23 '22 at 14:04
  • So you're saying that there is no longer ANY `restoreAllMocks` in your test? – Eric Haynes Jun 23 '22 at 17:56
0

I solved this by wrapping all calls to aws-skd functions in my own functions and then mocking my wrapper functions.

So now I have a utility functions in utils.js that looks like this:

module.exports.sendMessage = async (params) => {  
    return await sqs.sendMessage(params).promise()
}

And in my real function code, I call it this way (instead of calling sqs.sendMessage directly - a super-easy change):

await utils.sendMessage(sendMessageParams).catch(err => throw (err))

And then during testing, I mock it like this - either in a beforeEach or directly before my test:

sendMessageSpy = jest.spyOn(utils, 'sendMessage').mockReturnValue(Promise.resolve({success:true})) 

Later, rinse, and repeat for all aws-sdk functions I want to mock.

lowcrawler
  • 6,777
  • 9
  • 37
  • 79
  • 1
    This is often referred to as a "facade", and as well as being easier to mock decouples your code from any changes in the SDK interface (as they would then be reflected in the facade). Do make sure you have higher-level tests that use the real library too, though, or you might have passing tests but failing functionality (see [this example](https://stackoverflow.com/a/65627662/3001761)). – jonrsharpe Jun 27 '22 at 14:30