With Jest it’s possible to assert of single or specific arguments/parameters of a mock function call with
.toHaveBeenCalled
/.toBeCalled
andexpect.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.
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).
accountId | offset | limit | search | single-word search |
---|---|---|---|---|
Y | N | N | Y | Y |
Y | N | N | Y | N |
Y | N | Y | N | N/A |
Y | Y | Y | N | N/A |
Y | N | N | Y | Y |
Y | N | N | Y | N |
Y | Y | N | Y | Y |
Y | Y | N | Y | N |
Y | Y | Y | Y | Y |
Y | Y | Y | Y | N |
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:
- on search
- if search is not set,
pinger
should call with the default searchRegex - if search is set and is single word (no space),
pinger
should call with the correct searchRegex - if search is set and is multi-work (spaces),
pinger
should call with the correct searchRegex
- if search is not set,
- on limit/offset
- if limit/offset are not set,
pinger
should call with default values - if limit/offset are set,
pinger
should call with passed values
- if limit/offset are not set,
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.