How to expect (jest) a function to be called with an object that needs processing before it can be matched

Viewed 4261

I'm testing that a (constructor) function is called with a particular object, however the object itself has properties which themselves are JSON. This is because the third-party API which I am mocking expects this type of object, so I can't change it.

I want to test these JSON properties by converting them back to objects and using something like expect.objectContaining, because it is less brittle than trying to match the complete object, or worse still the JSON string.

I am testing an AWS Lambda SNS send function but I don't think that's entirely relevant, except to explain the syntax of my system under test:

export async function handler(event: any) {
    let sns = new SNSClient({ region: REGION });
    let myMessage = {}; // create my message
    let params = {
        Message: JSON.stringify(myMessage),
        TopicArn: 'my ARN'
    }

    const result = await sns.send(new PublishCommand(params));
    // some stuff
    return true;
}

My tests:

import { handler } from "./handler";
import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";

const mockSnsSend = jest.fn();
mockSnsSend.mockResolvedValue({ MessageId: 1 });
jest.mock('@aws-sdk/client-sns', () => {
    return {
        SNSClient: jest.fn().mockImplementation(() => {
            return {
                send: mockSnsSend
            }
        }),
        PublishCommand: jest.fn().mockImplementation()
    }
});


beforeEach(() => {
    // Clear all instances and calls to constructor and all methods:
    mockSnsSend.mockClear();
    (SNSClient as any).mockClear();
    (PublishCommand as any).mockClear();
    expect.hasAssertions();
});


test('Calls PublishCommand with correct message payload', async () => {
    let results = await handler(sampleLoraThingMessage);

    expect(PublishCommand).toHaveBeenCalledTimes(1);
    // what to do here?
});

What I've tried:

expect a string

This is easy and passes, but it is not sufficient:

expect(PublishCommand).toHaveBeenCalledWith(expect.objectContaining({
    Message: expect.any(String)
}));

extend expect

As per the docs I've tried extending expect to parse the JSON and then run an equals test over it, but I found that this is only available to the expect(a).hasJsonMatchingObject(b) syntax, and I can't use it with toHaveBeenCalledWith.

expect.extend({
    hasJsonMatchingObject(received, propertyName: string, actual: object) {
        let parsed = JSON.parse(received[propertyName]);
        let pass = this.equals(parsed, actual, undefined, false);
        return {
            pass: pass,
            // todo: I haven't handled the .not() case here yet
            message: () => `property ${propertyName} must have JSON with certain properties:` +
                '\n\n' +
                `Expected: not ${this.utils.printExpected(actual)}\n` +
                `Received: ${this.utils.printReceived(received)}`
        };
    }
})

...
expect(PublishCommand).toHaveBeenCalledWith(expect.hasJsonMatchingObject(...)); // expect has no property 'hasJsonMatchingObject'
1 Answers

In the process of writing this, I found an answer, and since I think I've put a bit of effort into it and I couldn't find it documented well elsewhere so I'm going to post and answer...

spy on argument

Jest gives you access to arguments passed to any mock implementation. So long as your function is mocked:

someFunc: jest.fn().mockImplementation()

someFunc.mock.calls gives access to parameters for each call to the function (docs):

let arg = someFunc.mock.calls[0][0]; // first call, first argument

then it's as simple as using any existing code to extract the properties for my test. Because I've mocked a module, I have to cast it back to a jest.Mock to satisfy Typescript:

// first make sure Message is a string
expect(PublishCommand).toHaveBeenCalledWith(expect.objectContaining({
    Message: expect.any(String)
}));
// then test Message.Message is a json object with certain properties
let arg = (PublishCommand as jest.Mock).mock.calls[0][0]; // first call, first argument
let actual = JSON.parse(arg.Message);
expect(actual).toMatchObject({
    'prop1': 'expected value',
    'prop2': 'expected value'
});
Related