5.4 Partial Parameter Assert: expect.anything()

With Jest it’s possible to assert of single or specific arguments/parameters of a mock function call with .toHaveBeenCalled/.toBeCalled and expect.anything().

You can use expect.anything() to ignore certain parameters that a mock Jest function is called with, see the following:

test('calls getPingConfigs with right accountId, searchRegex', async () => {
  await pinger(1);
  expect(mockPingConfig).toHaveBeenCalledWith(
    1,
    expect.anything(),
    expect.anything(),
    new RegExp('.*')
  );
});

Read on for more details of the code under test and why one would use such an approach.

Code under test that warrants specific parameter/argument assertions

From the following code under test which gets ping configs for each account and pings them all.

// Half-baked implementation of an uptime monitor
const getPingConfigs = jest.fn().mockReturnValue([]);
const fetch = jest.fn().mockResolvedValue({});

async function getUrlsForAccount(accountId, offset, limit, searchRegex) {
  const configs = await getPingConfigs(accountId, offset, limit, searchRegex);
  return configs.map(conf => conf.url);
}

async function pinger(accountId, { offset = 0, limit = 50 } = {}, search) {
  const searchRegex = search
    ? new RegExp(search.split(' ').join('|'))
    : new RegExp('.*');
  const urls = await getUrlsForAccount(accountId, offset, limit, searchRegex);
  return Promise.all(urls.map(url => fetch(url)));
}

Discovering orthogonality in code under test

We can see that there’s orthogonal functionality going on. Namely:

  • passing of accountId
  • computing/defaulting/passing of a search regex
  • defaulting/passing of offset/limit

All our tests will center around the values getPingConfigs is called with (using .toHaveBeenCalledWith assertions).

Let’s create some tests that don’t leverage expect.anything(), in every call, we’ll specify the value each of the parameters to getPingConfigs: accountId, offset, limit and searchRegex.

Permutations, (Y denotes the variable passed to pinger is set, N that it is not).

accountIdoffsetlimitsearchsingle-word search
YNNYY
YNNYN
YNYNN/A
YYYNN/A
YNNYY
YNNYN
YYNYY
YYNYN
YYYYY
YYYYN

Each of the above permutations should lead to different test cases if we have to specify each of the parameters/arguments in the assertion on the getPingConfigs call.

The enumeration we’ve done above would result in 10 test cases.

Creating test cases for orthogonal functionality

It turns out the following cases cover the same logic in a way that we care about:

  1. on search
    1. if search is not set, pinger should call with the default searchRegex
    2. if search is set and is single word (no space), pinger should call with the correct searchRegex
    3. if search is set and is multi-work (spaces), pinger should call with the correct searchRegex
  2. on limit/offset
    1. if limit/offset are not set, pinger should call with default values
    2. if limit/offset are set, pinger should call with passed values

Notice how the assertions only concern part of the call, which is where expect.anything() is going to come handy as a way to not have to assert over all the parameters/arguments of a mock call at the same time.

Specific parameter asserts on a mock function call

Given our pinger function.

const getPingConfigs = jest.fn().mockReturnValue([]);
const fetch = jest.fn().mockResolvedValue({});

async function getUrlsForAccount(accountId, offset, limit, searchRegex) {
  const configs = await getPingConfigs(accountId, offset, limit, searchRegex);
  return configs.map(conf => conf.url);
}

async function pinger(accountId, { offset = 0, limit = 50 } = {}, search) {
  const searchRegex = search
    ? new RegExp(search.split(' ').join('|'))
    : new RegExp('.*');
  const urls = await getUrlsForAccount(accountId, offset, limit, searchRegex);
  return Promise.all(urls.map(url => fetch(url)));
}

We can write the following tests a test for “without search” that will call pinger with just 1 as parameters.

describe('without search', () => {
  test('calls getPingConfigs with right accountId, searchRegex', async () => {
    await pinger(1);
    expect(getPingConfigs).toHaveBeenCalledWith(
      1,
      expect.anything(),
      expect.anything(),
      new RegExp('.*')
    );
  });
});

We can write tests for when offset and limit are called, asserting over what values are passed to getPingConfigs.

describe('offset, limit', () => {
  test('calls getPingConfigs with passed offset and limit', async () => {
    await pinger(1, { offset: 20, limit: 100 });
    expect(getPingConfigs).toHaveBeenCalledWith(1, 20, 100, expect.anything());
  });
  test('calls getPingConfigs with default offset and limit if undefined', async () => {
    await pinger(1);
    expect(getPingConfigs).toHaveBeenCalledWith(1, 0, 50, expect.anything());
  });
});

The same can be done for search and multi-word search

describe('search', () => {
  describe('single-word search', () => {
    test('calls getPingConfigs with right accountId, searchRegex', async () => {
      await pinger(1, {}, 'search');
      expect(getPingConfigs).toHaveBeenCalledWith(
        1,
        expect.anything(),
        expect.anything(),
        new RegExp('search')
      );
    });
  });
  describe('multi-word search', () => {
    test('calls getPingConfigs with right accountId, searchRegex', async () => {
      await pinger(1, {}, 'multi word search');
      expect(getPingConfigs).toHaveBeenCalledWith(
        1,
        expect.anything(),
        expect.anything(),
        new RegExp('multi|word|search')
      );
    });
  });
});

All the tests pass when run.

npx jest src/05.04-pinger.test.js
 PASS  src/05.04-pinger.test.js
  without search
    ✓ calls getPingConfigs with right accountId, searchRegex (4ms)
  offset, limit
    ✓ calls getPingConfigs with passed offset and limit (1ms)
    ✓ calls getPingConfigs with default offset and limit if undefined
  search
    single-word search
      ✓ calls getPingConfigs with right accountId, searchRegex
    multi-word search
      ✓ calls getPingConfigs with right accountId, searchRegex (1ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total

toHaveBeenCalledWith + objectContaining/arrayContaining

toHaveBeenCalledWith can be paired with objectContaing/arrayContaining and nested arrayContaining/objectContaining.

We can do simple array matches on parameters.

test('toHaveBeenCalledWith(arrayContaining)', () => {
  const myFunction = jest.fn();
  myFunction([1, 2, 3]);
  expect(myFunction).toHaveBeenCalledWith(expect.arrayContaining([2]));
});

We can also do simple object matches on parameters using expect.objectContaining.

test('toHaveBeenCalledWith(objectContaining)', () => {
  const myFunction = jest.fn();
  myFunction({
    name: 'Hugo',
    website: 'codewithhugo.com'
  });
  expect(myFunction).toHaveBeenCalledWith(
    expect.objectContaining({
      name: 'Hugo'
    })
  );
});

expect().toHaveBeenCalledWith() also supports nested expect.arrayContaining/expect.objectContaining to partially match inside of a parameter.

test('toHaveBeenCalledWith(nested object/array containing)', () => {
  const myFunction = jest.fn();
  myFunction([
    {age: 21, counsinIds: [1]},
    {age: 22, counsinIds: [1, 3]},
    {age: 23}
  ]);
  expect(myFunction).toHaveBeenCalledWith(
    expect.arrayContaining([
      expect.objectContaining({
        age: 22,
        counsinIds: expect.arrayContaining([3])
      })
    ])
  );
});

Output is all passing.

npx jest src/05.04-to-be-called-array-object-containing.test.js
 PASS  src/05.04-to-be-called-array-object-containing.test.js
  ✓ toHaveBeenCalledWith(arrayContaining) (4ms)
  ✓ toHaveBeenCalledWith(objectContaining) (1ms)
  ✓ toHaveBeenCalledWith(nested object/array containing)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total

We’ve now seen how to partially match parameters in a toHaveBeenCalledWith call.

We’ve also seen that expect.* methods work as toHaveBeenCalledWith parameters, so we can do partial matches on the individual parameters as well as partial match of the parameters.

Next, we’ll look at how to do even more generic matching using expect.any.

Jump to table of contents