Skip to content

Commit

Permalink
Add toHaveBeenCalledOnceWith matcher (#430)
Browse files Browse the repository at this point in the history
  • Loading branch information
thibautsabot committed Aug 16, 2022
1 parent 32ba52d commit 1f04716
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 0 deletions.
15 changes: 15 additions & 0 deletions README.md
Expand Up @@ -76,6 +76,7 @@ If you've come here to help contribute - Thanks! Take a look at the [contributin
- [.toHaveBeenCalledBefore()](#tohavebeencalledbefore)
- [.toHaveBeenCalledAfter()](#tohavebeencalledafter)
- [.toHaveBeenCalledOnce()](#tohavebeencalledonce)
- [.toHaveBeenCalledOnceWith()](#tohavebeencalledoncewith)
- [Number](#number)
- [.toBeNumber()](#tobenumber)
- [.toBeNaN()](#tobenan)
Expand Down Expand Up @@ -613,6 +614,20 @@ it('passes only if mock was called exactly once', () => {
});
```
#### .toHaveBeenCalledOnceWith()
Use `.toHaveBeenCalledOnceWith` to check if a `Mock` was called exactly one time with the expected value.
```js
it('passes only if mock was called exactly once with the expected value', () => {
const mock = jest.fn();

expect(mock).not.toHaveBeenCalled();
mock('hello');
expect(mock).toHaveBeenCalledOnceWith('hello');
});
```
### Number
#### .toBeNumber()
Expand Down
1 change: 1 addition & 0 deletions src/matchers/index.js
Expand Up @@ -51,6 +51,7 @@ export { toEqualCaseInsensitive } from './toEqualCaseInsensitive';
export { toHaveBeenCalledAfter } from './toHaveBeenCalledAfter';
export { toHaveBeenCalledBefore } from './toHaveBeenCalledBefore';
export { toHaveBeenCalledOnce } from './toHaveBeenCalledOnce';
export { toHaveBeenCalledOnceWith } from './toHaveBeenCalledOnceWith';
export { toInclude } from './toInclude';
export { toIncludeAllMembers } from './toIncludeAllMembers';
export { toIncludeAllPartialMembers } from './toIncludeAllPartialMembers';
Expand Down
47 changes: 47 additions & 0 deletions src/matchers/toHaveBeenCalledOnceWith.js
@@ -0,0 +1,47 @@
import { isJestMockOrSpy } from '../utils';

export function toHaveBeenCalledOnceWith(received, expected) {
const { printReceived, printExpected, printWithType, matcherHint } = this.utils;

if (!isJestMockOrSpy(received)) {
return {
pass: false,
message: () =>
matcherHint('.toHaveBeenCalledOnceWith') +
'\n\n' +
`Matcher error: ${printReceived('received')} must be a mock or spy function` +
'\n\n' +
printWithType('Received', received, printReceived),
};
}

const passMessage =
matcherHint('.not.toHaveBeenCalledOnceWith') +
'\n\n' +
`Expected mock function to have been called any amount of times but one with ${printExpected(
expected,
)}, but it was called exactly once with ${printExpected(expected)}.`;

const failOnceMessage =
matcherHint('.toHaveBeenCalledOnceWith') +
'\n\n' +
'Expected mock function to have been called exactly once, but it was called:\n' +
` ${printReceived(received.mock.calls.length)} times`;

const failExpectedMessage =
matcherHint('.toHaveBeenCalledOnceWith') +
'\n\n' +
`Expected mock function to have been called exactly once with ${printReceived(
expected,
)}, but it was called with:\n` +
` ${printReceived(received.mock.calls[0]?.[0])}`;

const passOnce = received.mock.calls.length === 1;
const pass = passOnce && this.equals(expected, received.mock.calls[0][0]);

return {
pass,
message: () => (pass ? passMessage : !passOnce ? failOnceMessage : failExpectedMessage),
actual: received,
};
}
37 changes: 37 additions & 0 deletions test/matchers/__snapshots__/toHaveBeenCalledOnceWith.test.js.snap
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`.not.toHaveBeenCalledOnceWith fails if mock was invoked exactly once with the expected value 1`] = `
"<dim>expect(</intensity><red>received</color><dim>).not.toHaveBeenCalledOnceWith(</intensity><green>expected</color><dim>)</intensity>
Expected mock function to have been called any amount of times but one with <green>\\"hello\\"</color>, but it was called exactly once with <green>\\"hello\\"</color>."
`;

exports[`.toHaveBeenCalledOnceWith fails if mock was invoked more than once, indicating how many times it was invoked 1`] = `
"<dim>expect(</intensity><red>received</color><dim>).toHaveBeenCalledOnceWith(</intensity><green>expected</color><dim>)</intensity>
Expected mock function to have been called exactly once, but it was called:
<red>17</color> times"
`;

exports[`.toHaveBeenCalledOnceWith fails if mock was never invoked indicating that it was invoked 0 times 1`] = `
"<dim>expect(</intensity><red>received</color><dim>).toHaveBeenCalledOnceWith(</intensity><green>expected</color><dim>)</intensity>
Expected mock function to have been called exactly once, but it was called:
<red>0</color> times"
`;

exports[`.toHaveBeenCalledOnceWith fails when given value is not a jest spy or mock 1`] = `
"<dim>expect(</intensity><red>received</color><dim>).toHaveBeenCalledOnceWith(</intensity><green>expected</color><dim>)</intensity>
Matcher error: <red>\\"received\\"</color> must be a mock or spy function
Received has type: function
Received has value: <red>[Function mock1]</color>"
`;

exports[`.toHaveBeenCalledOnceWith fails when given value is not the expected one 1`] = `
"<dim>expect(</intensity><red>received</color><dim>).toHaveBeenCalledOnceWith(</intensity><green>expected</color><dim>)</intensity>
Expected mock function to have been called exactly once with <red>\\"hello\\"</color>, but it was called with:
<red>\\"not hello\\"</color>"
`;
62 changes: 62 additions & 0 deletions test/matchers/toHaveBeenCalledOnceWith.test.js
@@ -0,0 +1,62 @@
import * as matcher from 'src/matchers/toHaveBeenCalledOnceWith';

expect.extend(matcher);

describe('.toHaveBeenCalledOnceWith', () => {
let mock;
beforeEach(() => {
mock = jest.fn();
});

test('passes if mock was invoked exactly once with the expected value', () => {
mock('hello');
expect(mock).toHaveBeenCalledOnceWith('hello');
});

test('fails if mock was never invoked indicating that it was invoked 0 times', () => {
expect(() => expect(mock).toHaveBeenCalledOnceWith('hello')).toThrowErrorMatchingSnapshot();
});

test('fails if mock was invoked more than once, indicating how many times it was invoked', () => {
// Invoke mock 17 times
new Array(17).fill(mock).forEach(e => e(Math.random()));
expect(() => expect(mock).toHaveBeenCalledOnceWith('hello')).toThrowErrorMatchingSnapshot();
});

test('fails when given value is not a jest spy or mock', () => {
const mock1 = () => {};
expect(() => expect(mock1).toHaveBeenCalledOnceWith('hello')).toThrowErrorMatchingSnapshot();
});

test('fails when given value is not the expected one', () => {
mock('not hello');
expect(() => expect(mock).toHaveBeenCalledOnceWith('hello')).toThrowErrorMatchingSnapshot();
});
});

describe('.not.toHaveBeenCalledOnceWith', () => {
let mock;
beforeEach(() => {
mock = jest.fn();
});

test('passes if mock was never invoked', () => {
expect(mock).not.toHaveBeenCalledOnceWith('hello');
});

test('passes if mock was invoked more than once', () => {
mock('hello');
mock('hello');
expect(mock).not.toHaveBeenCalledOnceWith('hello');
});

test('fails if mock was invoked exactly once with the expected value', () => {
mock('hello');
expect(() => expect(mock).not.toHaveBeenCalledOnceWith('hello')).toThrowErrorMatchingSnapshot();
});

test('passes if mock was invoked exactly once without the expected value', () => {
mock('not hello');
expect(mock).not.toHaveBeenCalledOnceWith('hello');
});
});
10 changes: 10 additions & 0 deletions types/index.d.ts
Expand Up @@ -171,6 +171,11 @@ declare namespace jest {
*/
toHaveBeenCalledOnce(): R;

/**
* Use `.toHaveBeenCalledOnceWith` to check if a `Mock` was called exactly one time with the expected value.
*/
toHaveBeenCalledOnceWith(): R;

/**
* Use `.toBeNumber` when checking if a value is a `Number`.
*/
Expand Down Expand Up @@ -601,6 +606,11 @@ declare namespace jest {
*/
toHaveBeenCalledOnce(): Result;

/**
* Use `.toHaveBeenCalledOnceWith` to check if a `Mock` was called exactly one time with the expected value.
*/
toHaveBeenCalledOnceWith(): Result;

/**
* Use `.toBeNumber` when checking if a value is a `Number`.
*/
Expand Down

0 comments on commit 1f04716

Please sign in to comment.