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).
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
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
.