(Updated: )
/ #testing #jest #node 

Jest assert over single or specific argument/parameters with .toHaveBeenCalledWith and expect.anything()

Curious about Advanced Jest Testing Features?

Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.

Get "The Jest Handbook" (100 pages)

I want this

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

The full example repository is at github.com/HugoDF/jest-specific-argument-assert, more specifically lines 17-66 in the src/pinger.test.js file.

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.

The code under test follows module boundaries similar to what is described in An enterprise-style Node.js REST API setup with Docker Compose, Express and Postgres. Specifically a 3-tier (Presentation, Domain, Data) layering, where we’ve only implemented the domain and (fake) data layers.

Table of Contents

Code under test that warrants specific parameter/argument assertions

The code under test is the following (see the full src/pinger.js file on GitHub), only relevant code has been included to make it obvious what problem we’ll be tackling with Jest mocks, .toHaveBeenCalled and expect.anything().

// Half-baked implementation of an uptime monitor
const { getPingConfigs } = require('./pingConfig');

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);
}

module.exports = pinger;

The only call going outside the module’s private context is getPingConfigs(accountId, offset, limit, searchRegex). This is why the assertion is going to be on the getPingConfigs mock that we’ve set with jest.mock('./pingConfig', () => {}) (see the full src/pinger.test.js code on GitHub).

Discovering orthogonality in code under test

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

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

Issues with exhaustive test cases for orthogonal functionality

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

The following implements the test cases we’ve defined in “Creating test cases for orthogonal functionality”:

describe('without search', () => {
  test('calls getPingConfigs with right accountId, searchRegex', async () => {
    await pinger(1);
    expect(mockPingConfig).toHaveBeenCalledWith(
      1,
      expect.anything(),
      expect.anything(),
      new RegExp('.*')
    );
  });
});
describe('offset, limit', () => {
  test('calls getPingConfigs with passed offset and limit', async () => {
    await pinger(1, { offset: 20, limit: 100 });
    expect(mockPingConfig).toHaveBeenCalledWith(
      1,
      20,
      100,
      expect.anything()
    );
  });
  test('calls getPingConfigs with default offset and limit if undefined', async () => {
    await pinger(1);
    expect(mockPingConfig).toHaveBeenCalledWith(1, 0, 50, expect.anything());
  });
});
describe('search', () => {
  describe('single-word search', () => {
    test('calls getPingConfigs with right accountId, searchRegex', async () => {
      await pinger(1, {}, 'search');
      expect(mockPingConfig).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(mockPingConfig).toHaveBeenCalledWith(
        1,
        expect.anything(),
        expect.anything(),
        new RegExp('multi|word|search')
      );
    });
  });
});

Further reading

Head over to github.com/HugoDF/jest-specific-argument-assert to see the full code and test suite. This includes code and tests that aren’t relevant to illustrate the concept of specific argument/parameter assertions with Jest .toHaveBeenCalledWith/.toBeCalled and expect.anything().

The way the code is written loosely follows what is described in An enterprise-style Node.js REST API setup with Docker Compose, Express and Postgres. Specifically a 3-tier (Presentation, Domain, Data) layering, where we’ve only implemented the domain and (fake) data layers.

Author

Hugo Di Francesco

Co-author of "Professional JavaScript", "Front-End Development Projects with Vue.js" with Packt, "The Jest Handbook" (self-published). Hugo runs the Code with Hugo website helping over 100,000 developers every month and holds an MEng in Mathematical Computation from University College London (UCL). He has used JavaScript extensively to create scalable and performant platforms at companies such as Canon, Elsevier and (currently) Eurostar.

Curious about Advanced Jest Testing Features?

Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library. Get "The Jest Handbook" (100 pages)