diff --git a/CHANGELOG.md b/CHANGELOG.md index f88c7a836577..1ea43efcfcf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## master +### features + +* `[jest-mock]` Add util methods to create async functions. + ([#5318](https://github.com/facebook/jest/pull/5318)) + ### Fixes * `[jest]` Add `import-local` to `jest` package. diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index 2600505e7d0d..249ef01f5a75 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -241,3 +241,84 @@ const myMockFn = jest.fn() console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()); > 'first call', 'second call', 'default', 'default' ``` + +### `mockFn.mockResolvedValue(value)` + +Simple sugar function for: + +```js +jest.fn().mockReturnValue(Promise.resolve(value)); +``` + +Useful to mock async functions in async tests: + +```js +test('async test', async () => { + const asyncMock = jest.fn().mockResolvedValue(43); + + await asyncMock(); // 43 +}); +``` + +### `mockFn.mockRejectedValueOnce(value)` + +Simple sugar function for: + +```js +jest.fn().mockReturnValueOnce(Promise.resolve(value)); +``` + +Useful to resolve different values over multiple async calls: + +```js +test('async test', async () => { + const asyncMock = jest.fn() + .mockResolvedValue('default') + .mockResolvedValueOnce('first call') + .mockResolvedValueOnce('second call'); + + await asyncMock(); // first call + await asyncMock(); // second call + await asyncMock(); // default + await asyncMock(); // default +}); +``` + +### `mockFn.mockRejectedValue(value)` + +Simple sugar function for: + +```js +jest.fn().mockReturnValue(Promise.reject(value)); +``` + +Useful to create async mock functions that will always reject: + +```js +test('async test', async () => { + const asyncMock = jest.fn().mockRejectedValue(new Error('Async error')); + + await asyncMock(); // throws "Async error" +}); +``` + +### `mockFn.mockRejectedValueOnce(value)` + +Simple sugar function for: + +```js +jest.fn().mockReturnValueOnce(Promise.reject(value)); +``` + +Example usage: + +```js +test('async test', async () => { + const asyncMock = jest.fn() + .mockResolvedValueOnce('first call') + .mockRejectedValueOnce(new Error('Async error')); + + await asyncMock(); // first call + await asyncMock(); // throws "Async error" +}); +``` diff --git a/packages/jest-mock/src/__tests__/jest_mock.test.js b/packages/jest-mock/src/__tests__/jest_mock.test.js index d298cba993de..ee692bc7efc2 100644 --- a/packages/jest-mock/src/__tests__/jest_mock.test.js +++ b/packages/jest-mock/src/__tests__/jest_mock.test.js @@ -391,6 +391,53 @@ describe('moduleMocker', () => { expect(fake(2)).toEqual(4); }); + it('supports mocking resolvable async functions', () => { + const fn = moduleMocker.fn(); + fn.mockResolvedValue('abcd'); + + const promise = fn(); + + expect(promise).toBeInstanceOf(Promise); + + return expect(promise).resolves.toBe('abcd'); + }); + + it('supports mocking resolvable async functions only once', () => { + const fn = moduleMocker.fn(); + fn.mockResolvedValue('abcd'); + fn.mockResolvedValueOnce('abcde'); + + return Promise.all([ + expect(fn()).resolves.toBe('abcde'), + expect(fn()).resolves.toBe('abcd'), + ]); + }); + + it('supports mocking rejectable async functions', () => { + const err = new Error('rejected'); + const fn = moduleMocker.fn(); + fn.mockRejectedValue(err); + + const promise = fn(); + + expect(promise).toBeInstanceOf(Promise); + + return expect(promise).rejects.toBe(err); + }); + + it('supports mocking rejectable async functions only once', () => { + const defaultErr = new Error('default rejected'); + const err = new Error('rejected'); + const fn = moduleMocker.fn(); + fn.mockRejectedValue(defaultErr); + fn.mockRejectedValueOnce(err); + + return Promise.all([ + expect(fn()).rejects.toBe(err), + expect(fn()).rejects.toBe(defaultErr), + ]); + }); + describe('timestamps', () => { const RealDate = Date; diff --git a/packages/jest-mock/src/index.js b/packages/jest-mock/src/index.js index 7490110d5de1..7d658ee5dab2 100644 --- a/packages/jest-mock/src/index.js +++ b/packages/jest-mock/src/index.js @@ -227,6 +227,17 @@ function getSlots(object?: Object): Array { return Object.keys(slots); } +function wrapAsyncParam( + fn: any => any, + asyncAction: 'resolve' | 'reject', +): any => any { + if (asyncAction === 'reject') { + return value => fn(Promise.reject(value)); + } + + return value => fn(Promise.resolve(value)); +} + class ModuleMockerClass { _environmentGlobal: Global; _mockState: WeakMap; @@ -407,6 +418,13 @@ class ModuleMockerClass { return f; }; + f.mockResolvedValueOnce = wrapAsyncParam( + f.mockReturnValueOnce, + 'resolve', + ); + + f.mockRejectedValueOnce = wrapAsyncParam(f.mockReturnValueOnce, 'reject'); + f.mockReturnValue = value => { // next function call will return specified return value or this one const mockConfig = this._ensureMockConfig(f); @@ -415,6 +433,10 @@ class ModuleMockerClass { return f; }; + f.mockResolvedValue = wrapAsyncParam(f.mockReturnValue, 'resolve'); + + f.mockRejectedValue = wrapAsyncParam(f.mockReturnValue, 'reject'); + f.mockImplementationOnce = fn => { // next function call will use this mock implementation return value // or default mock implementation return value