184

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.

EDIT: I want to highlight that all of the answers here are very useful and you can pick whichever suit your use-case. Thank you all - these answers are great!

dotnetCarpenter
  • 10,019
  • 6
  • 32
  • 54

5 Answers5

269

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)
  })
)
Roman Usherenko
  • 4,659
  • 4
  • 17
  • 14
46

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.

Herman Starikov
  • 2,636
  • 15
  • 26
  • 4
    It seems you can use `expect.anything` also: https://jestjs.io/docs/en/expect.html#expectanything – cl0udw4lk3r Jul 15 '19 at 14:31
  • @cl0udw4lk3r `expect.anything()` does not check what your calling parameter is, it simply checks that they is a parameter. – lbragile Jan 01 '21 at 23:53
27

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.

Liam
  • 27,717
  • 28
  • 128
  • 190
19

My favorite way. You can use the spread operator ... to expand the object you are checking then overwrite (or add) 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)
  })
0

For later people only want to match parts of the object's inner object.

In my case,

received arguments

{
  data: {
    attributes: {
      address: 'test address',
      area: 10000,
      'area-unit': 'square_feet',
      industry: 'agricultural',
      latitude: 30,
      longitude: 104,
      name: 'test',
      timezone: 'Asia/Shanghai',
    },
    relationships: {
      account: { data: { id: '96', type: 'accounts' } },
      users: {
        data: [
          { id: '1180', type: 'users' },
          { id: '1281', type: 'users' },
        ],
      },
    },
    type: 'buildings',
  },
};

And what I want to match is data.attributes. So I could use the code below to get the arguments of the called function ( my function only has one argument, if you want to test the 2nd argument of the first call, it will be mockFn.mock.calls[0][1])

const arg0 = mockFn.mock.calls[0][0];
expect(arg0.data.attributes).toEqual({
      address: 'test address',
      area: 10000,
      'area-unit': 'square_feet',
      industry: 'agricultural',
      latitude: 30,
      longitude: 104,
      name: 'test',
      timezone: 'Asia/Shanghai',
    });

I think it's more flexible than expect.objectContaining(object) or .toMatchObject() for some cases.

Leslie Wu
  • 84
  • 5