0

The function I'm looking to test receives a async function as an argument. In my argument validation I verify that it exists, is a function, and is an async function like so:

exports.consumeRecords = async (consumerFunction, records) => {

    if (!consumerFunction) {
        throw new Error(`Missing required argument, consumerFunction, in consumeRecords function`)
    }
    if (typeof consumerFunction !== 'function') {
        throw new Error(`Argument, consumerFunction, is not of type 'function' in consumeRecords function.  Type found: ${typeof consumerFunction}`)
    }
    if (args.consumerFunction.then instanceof Promise) {
        throw new Error(`Argument, consumerFunction, is not a async function in consumeRecords function.`)
    }
    await consumerFunction(records)
// ... blah blah ..
}

My tests for the above code look like this:

describe('FUNCTION: consumeRecords', () => {
    let logSpy, mockAsyncConsumerFunction
    beforeEach(()=> {
        mockAsyncConsumerFunction = async (recordBody) => `${recordBody} - resolved consumer`
        logSpy = jest.spyOn(console, 'log').mockImplementation(() => null)
    })
        test('throws error when missing consumerFunction argument', async () => {
            await expect(async () => await utils.consumeRecords({})).rejects.toThrow(`Missing required argument, consumerFunction,`)
        })
        test('throws error when consumerFunction is not a function', async () => {
            await expect(async () => await utils.consumeRecords({ consumerFunction: 123 })).rejects.toThrow(`Argument, consumerFunction, is not of type 'function'`)
        })
        test('throws error when consumerFunction is not async function', async () => {
            let syncFunction = () => console.log()
            await expect(async () => await utils.consumeRecords({ consumerFunction: syncFunction })).rejects.toThrow(`Argument, consumerFunction, is not a async function`)
        })
    afterEach(() => {
        jest.clearAllMocks()
    })
    afterAll(() => {
        jest.restoreAllMocks()
    })
})

Now, I'd like to test if consumerFunction gets called by spying on the mock, so I'm trying to do this at the top of my test:

mockAsyncConsumerFunction = async (recordBody) => `${recordBody} - resolved consumer`
mockAsyncConsumerFunctionSpy = jest.fn(mockAsyncConsumerFunction)

and then the standard assertions using the .mocks object on the jest.fn, like this:

test('calls consumer function correctly', async () => {
    await utils.consumeRecords({ consumerFunction: mockAsyncConsumerFunctionSpy, records: [{ body: JSON.stringify({fake:111}) },{ body: JSON.stringify({fake:222}) }], taskNameString: "Apply image overlays" })
    expect(mockAsyncConsumerFunctionSpy).toHaveBeenCalledWith({fake:111})
})

Unfortunately, after doing this, my test fails because it's no longer seen as an async function and thus my input validation fails, giving me:

FUNCTION: consumeRecords › calls consumer function correct number of
 times

    Argument, consumerFunction, is not a async function in consumeRecords function.

How can I make a mock/spy function in Jest that reads as an async function?

lowcrawler
  • 6,777
  • 9
  • 37
  • 79
  • 2
    All that validation seems very fussy, and would probably be better applied as _types_ (so the caller gets much earlier feedback). And why does it matter that it's an _async_ function? You can write everything you'd write with an async function with a regular function that returns a promise (which is what the type would be, and note you seem to ignore that promise either way. – jonrsharpe Jul 07 '22 at 07:01
  • 1
    But also note your test doubles don't _have_ to be made by Jest. You can just pass an _actual async function_, which e.g. assigns what it received to a local variable within the test you can then assert on. – jonrsharpe Jul 07 '22 at 07:05
  • 1
    "*`consumerFunction.constructor.name !== 'AsyncFunction'`*" - please don't do that. Validate that the function returns a thenable when called, that's all what matters. – Bergi Jul 07 '22 at 07:59
  • @Bergi I agree, that would be better. How would I test what a function returns? – lowcrawler Jul 07 '22 at 20:23
  • @jonrsharpe Do you have an example of validation "applied as types" or an article I can look at to learn more? Secondly - I think I HAVE created a non-jest function that is async, but how would I spy on if that non-jest function was called? – lowcrawler Jul 07 '22 at 20:26
  • 1
    1. https://www.typescriptlang.org/ 2. You don't have to spy on it to determine if it got called - again, you could simply assign what it's called with to a test-local variable, then assert it has the expected value. – jonrsharpe Jul 07 '22 at 20:28
  • @johnrsharpe Do you have a link to show or further explain how to do this: "2. You don't have to spy on it to determine if it got called - again, you could simply assign what it's called with to a test-local variable, then assert it has the expected value". I am already assigning the function to local variable... I don't know how to see if was called correctly. Thanks! (feel free to put it in an answer and I'll mark it answered) – lowcrawler Jul 07 '22 at 20:31
  • 1
    @jonrsharpe like [this](https://stackoverflow.com/a/55167607/1048572) or like [that](https://stackoverflow.com/a/27746373/1048572) or [with `instanceof`](https://stackoverflow.com/a/43034043/1048572), or just not at all, since your code also works with functions that don't return a promise – Bergi Jul 07 '22 at 20:32
  • @Bergi Thanks. I have changed my code above with the suggested change. (it doesn't solve how to spy on it, but I really appreciate the advice) – lowcrawler Jul 07 '22 at 20:41
  • 1
    _"I am already assigning the function to local variable"_ - sure, but what I said and you quoted was assign **what it's called with**. This isn't specific to Jest, or even really test doubles, just simple JS - e.g. `async (arg) => { someVar = arg }`. But again this is all working around a problem you've made for yourself. – jonrsharpe Jul 07 '22 at 20:41
  • 1
    No, don't test `args.consumerFunction.then instanceof Promise`, that doesn't make any sense. It's suppposed to be `const result = consumerFunction(records); if (typeof result?.then != 'function') throw new TypeError('Did not return a thenable'); if (!(result instanceof Promise)) throw new TypeError('Did not return a promise'); await result;` – Bergi Jul 07 '22 at 20:45
  • @johnrsharpe Thanks, that's more clear. To further clarity, you say I've created this problem for myself and the solution is "use Typescript", right? – lowcrawler Jul 07 '22 at 20:46
  • @Bergi Correct me if I'm wrong, but your code would result in needing to CALL the consumerFunction as part of the validation process? (which won't work because the consumerFunction has one-time-only side effects of consuming messages...) – lowcrawler Jul 07 '22 at 20:48
  • 2
    @lowcrawler Yes of course. You can't know what it will return without calling it. What exactly, and for what purpose, are you trying to "validate"? – Bergi Jul 07 '22 at 20:50
  • If I'm passed a non-async function, I want to throw an error -- rather than potentially consuming messages as part of a `Promise.allSettled` erroneously. I'm trying to error BEFORE anything is consumed/called by ensuring all arguments are as correct as possible before starting the business logic of the function. – lowcrawler Jul 07 '22 at 20:54
  • 1
    Again, you should not care whether the function is async or not. Your code works fine with synchronous functions, it should accept them! – Bergi Jul 07 '22 at 20:58
  • I can do something like `let settledPromises = await Promise.allSettled(records.map(async (record) => consumerFunction(record)))` regardless of if consumerFunction is async or sync? If that's the case, then I can avoid the issue in the OP! :) – lowcrawler Jul 07 '22 at 21:05
  • 1
    Yes you can! Possibly even `const results = await Promise.allSettled(records.map(consumerFunction))` – Bergi Jul 07 '22 at 21:08
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/246260/discussion-between-lowcrawler-and-bergi). – lowcrawler Jul 07 '22 at 21:08

0 Answers0