diff --git a/README.md b/README.md index 08e65f73..bcddddb3 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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() diff --git a/src/matchers/index.js b/src/matchers/index.js index 4ab5fc11..7d15a604 100644 --- a/src/matchers/index.js +++ b/src/matchers/index.js @@ -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'; @@ -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; diff --git a/src/matchers/toHaveBeenCalledOnce/__snapshots__/index.test.js.snap b/src/matchers/toHaveBeenCalledOnce/__snapshots__/index.test.js.snap new file mode 100644 index 00000000..9079ad90 --- /dev/null +++ b/src/matchers/toHaveBeenCalledOnce/__snapshots__/index.test.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`.not.toHaveBeenCalledOnce fails if mock was invoked exactly once 1`] = ` +"expect(received).not.toHaveBeenCalledOnce(expected) + +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`] = ` +"expect(received).toHaveBeenCalledOnce(expected) + +Expected mock function to have been called exactly once, but it was called: + 17 times" +`; + +exports[`.toHaveBeenCalledOnce fails if mock was never invoked indicating that it was invoked 0 times 1`] = ` +"expect(received).toHaveBeenCalledOnce(expected) + +Expected mock function to have been called exactly once, but it was called: + 0 times" +`; + +exports[`.toHaveBeenCalledOnce fails when given value is not a jest spy or mock 1`] = ` +"expect(received).toHaveBeenCalledAfter(expected) + +Matcher error: \\"received\\" must be a mock or spy function + +Received has type: function +Received has value: [Function mock1]" +`; diff --git a/src/matchers/toHaveBeenCalledOnce/index.js b/src/matchers/toHaveBeenCalledOnce/index.js new file mode 100644 index 00000000..1e2f5647 --- /dev/null +++ b/src/matchers/toHaveBeenCalledOnce/index.js @@ -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, + }; + }, +}; diff --git a/src/matchers/toHaveBeenCalledOnce/index.test.js b/src/matchers/toHaveBeenCalledOnce/index.test.js new file mode 100644 index 00000000..dd2328d8 --- /dev/null +++ b/src/matchers/toHaveBeenCalledOnce/index.test.js @@ -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(); + }); +}); diff --git a/src/matchers/toHaveBeenCalledOnce/predicate.js b/src/matchers/toHaveBeenCalledOnce/predicate.js new file mode 100644 index 00000000..df139ad0 --- /dev/null +++ b/src/matchers/toHaveBeenCalledOnce/predicate.js @@ -0,0 +1 @@ +export default mockFn => mockFn.mock.calls.length === 1; diff --git a/src/matchers/toHaveBeenCalledOnce/predicate.test.js b/src/matchers/toHaveBeenCalledOnce/predicate.test.js new file mode 100644 index 00000000..ba5d9e5d --- /dev/null +++ b/src/matchers/toHaveBeenCalledOnce/predicate.test.js @@ -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); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index 82cada72..6680c133 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -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`. */ @@ -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`. */