Skip to content

Commit

Permalink
Add matcher .toHaveBeenCalledOnce (#274)
Browse files Browse the repository at this point in the history
Co-authored-by: Igor Bykov <[email protected]>
  • Loading branch information
Uzwername and igor-repeatmd authored Oct 11, 2021
1 parent d5dd8f1 commit 405f1e7
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 0 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ If you've come here to help contribute - Thanks! Take a look at the [contributin
- [Mock](#mock)
- [.toHaveBeenCalledBefore()](#tohavebeencalledbefore)
- [.toHaveBeenCalledAfter()](#tohavebeencalledafter)
- [.toHaveBeenCalledOnce()](#tohavebeencalledonce)
- [Number](#number)
- [.toBeNumber()](#tobenumber)
- [.toBeNaN()](#tobenan)
Expand Down Expand Up @@ -533,6 +534,20 @@ it('calls mock1 after mock2', () => {
});
```
#### .toHaveBeenCalledOnce()
Use `.toHaveBeenCalledOnce` to check if a `Mock` was called exactly one time.
```js
it('passes only if mock was called exactly once', () => {
const mock = jest.fn();

expect(mock).not.toHaveBeenCalled();
mock();
expect(mock).toHaveBeenCalledOnce();
});
```
### Number
#### .toBeNumber()
Expand Down
2 changes: 2 additions & 0 deletions src/matchers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import toEndWithMatcher from './toEndWith';
import toEqualCaseInsensitiveMatcher from './toEqualCaseInsensitive';
import toHaveBeenCalledAfterMatcher from './toHaveBeenCalledAfter';
import toHaveBeenCalledBeforeMatcher from './toHaveBeenCalledBefore';
import toHaveBeenCalledOnceMatcher from './toHaveBeenCalledOnce';
import toIncludeMatcher from './toInclude';
import toIncludeAllMembersMatcher from './toIncludeAllMembers';
import toIncludeAllPartialMembersMatcher from './toIncludeAllPartialMembers';
Expand Down Expand Up @@ -113,6 +114,7 @@ export const toEndWith = toEndWithMatcher.toEndWith;
export const toEqualCaseInsensitive = toEqualCaseInsensitiveMatcher.toEqualCaseInsensitive;
export const toHaveBeenCalledAfter = toHaveBeenCalledAfterMatcher.toHaveBeenCalledAfter;
export const toHaveBeenCalledBefore = toHaveBeenCalledBeforeMatcher.toHaveBeenCalledBefore;
export const toHaveBeenCalledOnce = toHaveBeenCalledOnceMatcher.toHaveBeenCalledOnce;
export const toInclude = toIncludeMatcher.toInclude;
export const toIncludeAllMembers = toIncludeAllMembersMatcher.toIncludeAllMembers;
export const toIncludeAllPartialMembers = toIncludeAllPartialMembersMatcher.toIncludeAllPartialMembers;
Expand Down
30 changes: 30 additions & 0 deletions src/matchers/toHaveBeenCalledOnce/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`.not.toHaveBeenCalledOnce fails if mock was invoked exactly once 1`] = `
"<dim>expect(</><red>received</><dim>).not.toHaveBeenCalledOnce(</><green>expected</><dim>)</>
Expected mock function to have been called any amount of times but one, but it was called exactly once."
`;
exports[`.toHaveBeenCalledOnce fails if mock was invoked more than once, indicating how many times it was invoked 1`] = `
"<dim>expect(</><red>received</><dim>).toHaveBeenCalledOnce(</><green>expected</><dim>)</>
Expected mock function to have been called exactly once, but it was called:
<red>17</> times"
`;
exports[`.toHaveBeenCalledOnce fails if mock was never invoked indicating that it was invoked 0 times 1`] = `
"<dim>expect(</><red>received</><dim>).toHaveBeenCalledOnce(</><green>expected</><dim>)</>
Expected mock function to have been called exactly once, but it was called:
<red>0</> times"
`;
exports[`.toHaveBeenCalledOnce fails when given value is not a jest spy or mock 1`] = `
"<dim>expect(</><red>received</><dim>).toHaveBeenCalledAfter(</><green>expected</><dim>)</>
Matcher error: <red>\\"received\\"</> must be a mock or spy function
Received has type: function
Received has value: <red>[Function mock1]</>"
`;
45 changes: 45 additions & 0 deletions src/matchers/toHaveBeenCalledOnce/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { matcherHint, printReceived, printWithType } from 'jest-matcher-utils';

import { isJestMockOrSpy } from '../../utils';

import predicate from './predicate';

const passMessage = () => () =>
matcherHint('.not.toHaveBeenCalledOnce') +
'\n\n' +
'Expected mock function to have been called any amount of times but one, but it was called exactly once.';

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

const mockCheckFailMessage = value => () => {
return (
matcherHint('.toHaveBeenCalledAfter') +
'\n\n' +
`Matcher error: ${printReceived('received')} must be a mock or spy function` +
'\n\n' +
printWithType('Received', value, printReceived)
);
};

export default {
toHaveBeenCalledOnce: received => {
if (!isJestMockOrSpy(received)) {
return { pass: false, message: mockCheckFailMessage(received) };
}

const pass = predicate(received);

return {
pass,
message: pass ? passMessage(received) : failMessage(received),
actual: received,
};
},
};
52 changes: 52 additions & 0 deletions src/matchers/toHaveBeenCalledOnce/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import matcher from './';

expect.extend(matcher);

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

test('passes if mock was invoked exactly once', () => {
mock();
expect(mock).toHaveBeenCalledOnce();
});

test('fails if mock was never invoked indicating that it was invoked 0 times', () => {
expect(() => expect(mock).toHaveBeenCalledOnce()).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).toHaveBeenCalledOnce()).toThrowErrorMatchingSnapshot();
});

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

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

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

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

test('fails if mock was invoked exactly once', () => {
mock();
expect(() => expect(mock).not.toHaveBeenCalledOnce()).toThrowErrorMatchingSnapshot();
});
});
1 change: 1 addition & 0 deletions src/matchers/toHaveBeenCalledOnce/predicate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default mockFn => mockFn.mock.calls.length === 1;
25 changes: 25 additions & 0 deletions src/matchers/toHaveBeenCalledOnce/predicate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import predicate from './predicate';

describe('.toHaveBeenCalledOnce predicate', () => {
let mock;
beforeEach(() => {
// Refresh on each test
mock = jest.fn();
});

test('returns true if mock was invoked exactly once', () => {
mock();
expect(predicate(mock)).toBe(true);
});

test('returns true if mock was invoked any amount of times but one', () => {
expect(predicate(mock)).toBe(false);

mock();
mock();
expect(predicate(mock)).toBe(false);

new Array(20).fill(mock).forEach(e => e());
expect(predicate(mock)).toBe(false);
});
});
10 changes: 10 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ declare namespace jest {
*/
toHaveBeenCalledAfter(mock: jest.Mock): R;

/**
* Use `.toHaveBeenCalledOnce` to check if a `Mock` was called exactly one time.
*/
toHaveBeenCalledOnce(): R;

/**
* Use `.toBeNumber` when checking if a value is a `Number`.
*/
Expand Down Expand Up @@ -541,6 +546,11 @@ declare namespace jest {
*/
toHaveBeenCalledAfter(mock: jest.Mock): any;

/**
* Use `.toHaveBeenCalledOnce` to check if a `Mock` was called exactly one time.
*/
toHaveBeenCalledOnce(): any;

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

0 comments on commit 405f1e7

Please sign in to comment.