Loose match one value in jest.toHaveBeenCalledWith
I have an analytics tracker that will only call after 1 second and with an object where the intervalInMilliseconds
(duration) value is not deterministic.
How can I use jest.toHaveBeenCalledWith
to test the object?
test('pageStats - publicationPage (will wait 1000ms)', done => {
const track = jest.fn()
const expected = new PayloadTiming({
category: 'PublicationPage',
action: 'PublicationPage',
name: 'n/a',
label: '7',
intervalInMilliseconds: 1000 // or around
})
mockInstance.viewState.layoutMode = PSPDFKit.LayoutMode.SINGLE
const sendPageStats = pageStats({
instance: mockInstance,
track,
remoteId: nappConfig.remoteId
})
mockInstance.addEventListener('viewState.currentPageIndex.change', sendPageStats)
setTimeout(() => {
mockInstance.fire('viewState.currentPageIndex.change', 2)
expect(track).toHaveBeenCalled()
expect(track).toHaveBeenCalledWith(expected)
done()
}, 1000)
expect(track).not.toHaveBeenCalled()
})
expect(track).toHaveBeenCalledWith(expected)
fails with:
Expected mock function to have been called with:
{"action": "PublicationPage", "category": "PublicationPage", "intervalInMilliseconds": 1000, "label": "7", "name": "n/a"}
as argument 1, but it was called with
{"action": "PublicationPage", "category": "PublicationPage", "intervalInMilliseconds": 1001, "label": "7", "name": "n/a"}
I have looked at jest-extended but I do not see anything useful for my use-case.
Solution 1:
This can be done with asymmetric matchers (introduced in Jest 18)
expect(track).toHaveBeenCalledWith(
expect.objectContaining({
"action": "PublicationPage",
"category": "PublicationPage",
"label": "7",
"name": "n/a"
})
)
If you use jest-extended
you can do something like
expect(track).toHaveBeenCalledWith(
expect.objectContaining({
"action": "PublicationPage",
"category": "PublicationPage",
"label": "7",
"name": "n/a",
"intervalInMilliseconds": expect.toBeWithin(999, 1002)
})
)
Solution 2:
You can access the expected object for a better assertion using track.mock.calls[0][0]
(the first [0]
is the invocation number, and the second [0]
is the argument number). Then you could use toMatchObject
to find partially match the object, avoiding the dynamic parameters such as intervalInMilliseconds
.
Solution 3:
To re-iterate the comment by cl0udw4lk3r as I found this the most useful in my scenario:
If you have a method that accepts multiple parameters (not an object) and you only want to match some of these parameters then you can use the expect
object.
Example
method I want to test:
client.setex(key, ttl, JSON.stringify(obj));
I want to ensure the correct values are passed into the key
and ttl
but I'm not concerned what the object passed in is. So I set up a spy:
const setexSpy = jest.spyOn(mockClient, "setex");
and I can then expect this scenario thus:
expect(setexSpy).toHaveBeenCalledWith('test', 99, expect.anything());
You can also use more strongly typed calls using expect.any
(expect.any(Number)
) etc.
Solution 4:
Of course I'm biased but I think this is the best and cleanest way. You can use the spread operator ...
to expand the object you are checking then overwrite one or more values.
Here is an example showing how to overwrite the "intervalInMilliseconds" expected value to any Number
const track = jest.fn()
const expected = new PayloadTiming({
category: 'PublicationPage',
action: 'PublicationPage',
name: 'n/a',
label: '7',
intervalInMilliseconds: 1000 // or around
})
expect(track).toHaveBeenCalledWith(
{
...expected,
intervalInMilliseconds: expect.any(Number)
})
another example showing how to overwrite two values
expect(track).toHaveBeenCalledWith(
{
...expected,
intervalInMilliseconds: expect.any(Number),
category: expect.any(String)
})