diff --git a/README.md b/README.md index 894eafc..66bba98 100644 --- a/README.md +++ b/README.md @@ -284,10 +284,23 @@ fn(2); // Will throw a helpful jest assertion error with args diff #### Supports default behavior -Use any of `mockReturnValue`, `mockResolvedValue`, `mockRejectedValue`, `mockImplementation` directly on the object +Use any of `defaultReturnValue`, `defaultResolvedValue`, `defaultRejectedValue`, `defaultImplementation` to set up a default behavior, which will serve as fallback if no matcher fits. ```javascript +when(fn) + .calledWith('foo').mockReturnValue('special') + .defaultReturnValue('default') // This line can be placed anywhere, doesn't have to be at the end + +expect(fn('foo')).toEqual('special') +expect(fn('bar')).toEqual('default') +``` + +Or if you use any of `mockReturnValue`, `mockResolvedValue`, `mockRejectedValue`, `mockImplementation` directly on the object +before using `calledWith` it will also behave as a default fallback. + +```javascript +// Same as above example when(fn) .mockReturnValue('default') .calledWith('foo').mockReturnValue('special') @@ -299,15 +312,15 @@ expect(fn('bar')).toEqual('default') One idea is to set up a default implementation that throws an error if an improper call is made to the mock. ```javascript -// A default implementation that fails your test -const unsupportedCallError = (...args) => { - throw new Error(`Wrong args: ${JSON.stringify(args, null, 2)}`); -}; - when(fn) - .mockImplementation(unsupportedCallError) .calledWith(correctArgs) - .mockReturnValue(expectedValue); + .mockReturnValue(expectedValue) + .defaultImplementation(unsupportedCallError) + +// A default implementation that fails your test +function unsupportedCallError(...args) { + throw new Error(`Wrong args: ${JSON.stringify(args, null, 2)}`); +} ``` #### Supports custom mockImplementation diff --git a/src/when.js b/src/when.js index 760b9bb..7d72de9 100644 --- a/src/when.js +++ b/src/when.js @@ -62,11 +62,11 @@ class WhenMock { fn.__whenMock__ = this this.callMocks = [] this._origMock = fn.getMockImplementation() - this.defaultImplementation = null + this._defaultImplementation = null const _mockImplementation = (matchers, expectCall, once = false) => (mockImplementation) => { if (matchers[0] === NO_CALLED_WITH_YET) { - this.defaultImplementation = mockImplementation + this._defaultImplementation = mockImplementation } // To enable dynamic replacement during a test: // * call mocks with equal matchers are removed @@ -108,8 +108,8 @@ class WhenMock { } } - if (this.defaultImplementation) { - return this.defaultImplementation(...args) + if (this._defaultImplementation) { + return this._defaultImplementation(...args) } if (typeof fn.__whenMock__._origMock === 'function') { return fn.__whenMock__._origMock(...args) @@ -131,18 +131,26 @@ class WhenMock { mockRejectedValue: err => _mockImplementation(matchers, expectCall)(() => Promise.reject(err)), mockRejectedValueOnce: err => _mockImplementation(matchers, expectCall, true)(() => Promise.reject(err)), mockImplementation: implementation => _mockImplementation(matchers, expectCall)(implementation), - mockImplementationOnce: implementation => _mockImplementation(matchers, expectCall, true)(implementation) + mockImplementationOnce: implementation => _mockImplementation(matchers, expectCall, true)(implementation), + defaultImplementation: implementation => this.defaultImplementation(implementation), + defaultReturnValue: returnValue => this.defaultReturnValue(returnValue), + defaultResolvedValue: returnValue => this.defaultResolvedValue(returnValue), + defaultRejectedValue: err => this.defaultRejectedValue(err) }) // These four functions are only used when the dev has not used `.calledWith` before calling one of the mock return functions - this.mockImplementation = mockImplementation => { + this.defaultImplementation = mockImplementation => { // Set up an implementation with a special matcher that can never be matched because it uses a private symbol // Additionally the symbols existence can be checked to see if a calledWith was omitted. return _mockImplementation([NO_CALLED_WITH_YET], false)(mockImplementation) } - this.mockReturnValue = returnValue => this.mockImplementation(() => returnValue) - this.mockResolvedValue = returnValue => this.mockReturnValue(Promise.resolve(returnValue)) - this.mockRejectedValue = err => this.mockReturnValue(Promise.reject(err)) + this.defaultReturnValue = returnValue => this.defaultImplementation(() => returnValue) + this.defaultResolvedValue = returnValue => this.defaultReturnValue(Promise.resolve(returnValue)) + this.defaultRejectedValue = err => this.defaultResolvedValue(Promise.reject(err)) + this.mockImplementation = this.defaultImplementation + this.mockReturnValue = this.defaultReturnValue + this.mockResolvedValue = this.defaultResolvedValue + this.mockRejectedValue = this.defaultRejectedValue this.calledWith = (...matchers) => ({ ...mockFunctions(matchers, false) }) this.expectCalledWith = (...matchers) => ({ ...mockFunctions(matchers, true) }) diff --git a/src/when.test.js b/src/when.test.js index 1aa506a..de4eaf6 100644 --- a/src/when.test.js +++ b/src/when.test.js @@ -585,152 +585,339 @@ describe('When', () => { expect(fn.mock.calls.length).toBe(1) }) - it('has a default and a non-default behavior', () => { - const fn = jest.fn() + describe('Default Behavior (no called with)', () => { + describe('defaultX methods', () => { + it('has a default and a non-default behavior (defaultReturnValue alias)', () => { + let fn = jest.fn() - when(fn) - .mockReturnValue('default') - .calledWith('foo') - .mockReturnValue('special') + when(fn) + .defaultReturnValue('default') + .calledWith('foo') + .mockReturnValue('special') + + expect(fn('bar')).toEqual('default') + expect(fn('foo')).toEqual('special') + expect(fn('bar')).toEqual('default') - expect(fn('bar')).toEqual('default') - expect(fn('foo')).toEqual('special') - expect(fn('bar')).toEqual('default') - }) + fn = jest.fn() + + when(fn) + .calledWith('foo') + .mockReturnValue('special') + .defaultReturnValue('default') + + expect(fn('bar')).toEqual('default') + expect(fn('foo')).toEqual('special') + expect(fn('bar')).toEqual('default') + }) - it('has a default which is falsy', () => { - const fn = jest.fn() + it('has a default which is falsy (defaultReturnValue alias)', () => { + let fn = jest.fn() - when(fn) - .mockReturnValue(false) - .calledWith('foo') - .mockReturnValue('special') + when(fn) + .defaultReturnValue(false) + .calledWith('foo') + .mockReturnValue('special') - expect(fn('bar')).toEqual(false) - expect(fn('foo')).toEqual('special') - expect(fn('bar')).toEqual(false) - }) + expect(fn('bar')).toEqual(false) + expect(fn('foo')).toEqual('special') + expect(fn('bar')).toEqual(false) - it('has a default value which is a function', () => { - const fn = jest.fn() - const defaultValue = () => { } + fn = jest.fn() - when(fn) - .mockReturnValue(defaultValue) - .calledWith('bar').mockReturnValue('baz') + when(fn) + .calledWith('foo') + .mockReturnValue('special') + .defaultReturnValue(false) - expect(fn('foo')).toBe(defaultValue) - }) + expect(fn('bar')).toEqual(false) + expect(fn('foo')).toEqual('special') + expect(fn('bar')).toEqual(false) + }) - it('has a default implementation', () => { - const fn = jest.fn() + it('has a default value which is a function (defaultReturnValue alias)', () => { + const fn = jest.fn() + const defaultValue = () => { + } - when(fn) - .mockImplementation(() => 1) - .calledWith('bar').mockReturnValue('baz') + when(fn) + .defaultReturnValue(defaultValue) + .calledWith('bar').mockReturnValue('baz') - expect(fn('foo')).toBe(1) - expect(fn('bar')).toBe('baz') - }) + expect(fn('foo')).toBe(defaultValue) + }) - it('has access to args in a default implementation', () => { - const fn = jest.fn() + it('has a default implementation (defaultReturnValue alias)', () => { + let fn = jest.fn() - when(fn) - .mockImplementation(({ name }) => `Hello ${name}`) - .calledWith({ name: 'bar' }).mockReturnValue('Goodbye bar') + when(fn) + .defaultImplementation(() => 1) + .calledWith('bar').mockReturnValue('baz') - expect(fn({ name: 'foo' })).toBe('Hello foo') - expect(fn({ name: 'bar' })).toBe('Goodbye bar') - }) + expect(fn('foo')).toBe(1) + expect(fn('bar')).toBe('baz') - it('keeps the default with a lot of matchers', () => { - const fn = jest.fn() + fn = jest.fn() - when(fn) - .mockReturnValue('default') - .calledWith('in1').mockReturnValue('out1') - .calledWith('in2').mockReturnValue('out2') - .calledWith('in3').mockReturnValue('out3') - .calledWith('in4').mockReturnValueOnce('out4') + when(fn) + .calledWith('bar').mockReturnValue('baz') + .defaultImplementation(() => 1) - expect(fn('foo')).toEqual('default') - expect(fn('in2')).toEqual('out2') - expect(fn('in4')).toEqual('out4') - expect(fn('in1')).toEqual('out1') - expect(fn('in3')).toEqual('out3') - expect(fn('in4')).toEqual('default') - }) + expect(fn('foo')).toBe(1) + expect(fn('bar')).toBe('baz') + }) - it('has a default and non-default resolved value', async () => { - const fn = jest.fn() + it('has access to args in a default implementation (defaultReturnValue alias)', () => { + const fn = jest.fn() - when(fn) - .mockResolvedValue('default') - .calledWith('foo').mockResolvedValue('special') + when(fn) + .defaultImplementation(({ name }) => `Hello ${name}`) + .calledWith({ name: 'bar' }).mockReturnValue('Goodbye bar') - await expect(fn('bar')).resolves.toEqual('default') - await expect(fn('foo')).resolves.toEqual('special') - }) + expect(fn({ name: 'foo' })).toBe('Hello foo') + expect(fn({ name: 'bar' })).toBe('Goodbye bar') + }) - it('has a default and non-default rejected value', async () => { - const fn = jest.fn() + it('keeps the default with a lot of matchers (defaultReturnValue alias)', () => { + const fn = jest.fn() - when(fn) - .mockRejectedValue(new Error('default')) - .calledWith('foo').mockRejectedValue(new Error('special')) + when(fn) + .calledWith('in1').mockReturnValue('out1') + .calledWith('in2').mockReturnValue('out2') + .calledWith('in3').mockReturnValue('out3') + .calledWith('in4').mockReturnValueOnce('out4') + .defaultReturnValue('default') - await expect(fn('bar')).rejects.toThrow('default') - await expect(fn('foo')).rejects.toThrow('special') - }) + expect(fn('foo')).toEqual('default') + expect(fn('in2')).toEqual('out2') + expect(fn('in4')).toEqual('out4') + expect(fn('in1')).toEqual('out1') + expect(fn('in3')).toEqual('out3') + expect(fn('in4')).toEqual('default') + }) - it('default reject interoperates with resolve', async () => { - const fn = jest.fn() + it('has a default and non-default resolved value (defaultReturnValue alias)', async () => { + const fn = jest.fn() - when(fn) - .mockRejectedValue(new Error('non-mocked interaction')) - .calledWith('foo').mockResolvedValue('mocked') + when(fn) + .calledWith('foo').mockResolvedValue('special') + .defaultResolvedValue('default') - await expect(fn('foo')).resolves.toEqual('mocked') - await expect(fn('bar')).rejects.toThrow('non-mocked interaction') - }) + await expect(fn('bar')).resolves.toEqual('default') + await expect(fn('foo')).resolves.toEqual('special') + }) - it('can override default', () => { - const fn = jest.fn() + it('has a default and non-default rejected value (defaultReturnValue alias)', async () => { + const fn = jest.fn() - when(fn) - .mockReturnValue('oldDefault') - .mockReturnValue('newDefault') - .calledWith('foo').mockReturnValue('bar') + when(fn) + .calledWith('foo').mockRejectedValue(new Error('special')) + .defaultRejectedValue(new Error('default')) - expect(fn('foo')).toEqual('bar') - expect(fn('foo2')).toEqual('newDefault') - }) + await expect(fn('bar')).rejects.toThrow('default') + await expect(fn('foo')).rejects.toThrow('special') + }) - it('allows defining the default NOT in a chained case', async () => { - const fn = jest.fn() + it('default reject interoperates with resolve (defaultReturnValue alias)', async () => { + const fn = jest.fn() - when(fn).mockRejectedValue(false) + when(fn) + .calledWith('foo').mockResolvedValue('mocked') + .defaultRejectedValue(new Error('non-mocked interaction')) - when(fn) - .calledWith(expect.anything()) - .mockResolvedValue(true) + await expect(fn('foo')).resolves.toEqual('mocked') + await expect(fn('bar')).rejects.toThrow('non-mocked interaction') + }) - await expect(fn('anything')).resolves.toEqual(true) - await expect(fn()).rejects.toEqual(false) - }) + it('can override default (defaultReturnValue alias)', () => { + const fn = jest.fn() - it('allows overriding the default NOT in a chained case', () => { - const fn = jest.fn() + when(fn) + .defaultReturnValue('oldDefault') + .calledWith('foo').mockReturnValue('bar') + .defaultReturnValue('newDefault') - when(fn).mockReturnValue(1) - when(fn).mockReturnValue(2) + expect(fn('foo')).toEqual('bar') + expect(fn('foo2')).toEqual('newDefault') + }) - when(fn) - .calledWith(expect.anything()) - .mockReturnValue(true) + it('allows defining the default NOT in a chained case (defaultReturnValue alias)', async () => { + const fn = jest.fn() + + when(fn).defaultRejectedValue(false) + + when(fn) + .calledWith(expect.anything()) + .mockResolvedValue(true) + + await expect(fn('anything')).resolves.toEqual(true) + await expect(fn()).rejects.toEqual(false) + }) + + it('allows overriding the default NOT in a chained case (defaultReturnValue alias)', () => { + const fn = jest.fn() - expect(fn()).toEqual(2) + when(fn) + .calledWith(expect.anything()) + .mockReturnValue(true) + + when(fn).defaultReturnValue(1) + when(fn).defaultReturnValue(2) + + expect(fn()).toEqual(2) + }) + }) + + describe('legacy methods', () => { + it('has a default and a non-default behavior', () => { + const fn = jest.fn() + + when(fn) + .mockReturnValue('default') + .calledWith('foo') + .mockReturnValue('special') + + expect(fn('bar')).toEqual('default') + expect(fn('foo')).toEqual('special') + expect(fn('bar')).toEqual('default') + }) + + it('has a default which is falsy', () => { + const fn = jest.fn() + + when(fn) + .mockReturnValue(false) + .calledWith('foo') + .mockReturnValue('special') + + expect(fn('bar')).toEqual(false) + expect(fn('foo')).toEqual('special') + expect(fn('bar')).toEqual(false) + }) + + it('has a default value which is a function', () => { + const fn = jest.fn() + const defaultValue = () => { + } + + when(fn) + .mockReturnValue(defaultValue) + .calledWith('bar').mockReturnValue('baz') + + expect(fn('foo')).toBe(defaultValue) + }) + + it('has a default implementation', () => { + const fn = jest.fn() + + when(fn) + .mockImplementation(() => 1) + .calledWith('bar').mockReturnValue('baz') + + expect(fn('foo')).toBe(1) + expect(fn('bar')).toBe('baz') + }) + + it('has access to args in a default implementation', () => { + const fn = jest.fn() + + when(fn) + .mockImplementation(({ name }) => `Hello ${name}`) + .calledWith({ name: 'bar' }).mockReturnValue('Goodbye bar') + + expect(fn({ name: 'foo' })).toBe('Hello foo') + expect(fn({ name: 'bar' })).toBe('Goodbye bar') + }) + + it('keeps the default with a lot of matchers', () => { + const fn = jest.fn() + + when(fn) + .mockReturnValue('default') + .calledWith('in1').mockReturnValue('out1') + .calledWith('in2').mockReturnValue('out2') + .calledWith('in3').mockReturnValue('out3') + .calledWith('in4').mockReturnValueOnce('out4') + + expect(fn('foo')).toEqual('default') + expect(fn('in2')).toEqual('out2') + expect(fn('in4')).toEqual('out4') + expect(fn('in1')).toEqual('out1') + expect(fn('in3')).toEqual('out3') + expect(fn('in4')).toEqual('default') + }) + + it('has a default and non-default resolved value', async () => { + const fn = jest.fn() + + when(fn) + .mockResolvedValue('default') + .calledWith('foo').mockResolvedValue('special') + + await expect(fn('bar')).resolves.toEqual('default') + await expect(fn('foo')).resolves.toEqual('special') + }) + + it('has a default and non-default rejected value', async () => { + const fn = jest.fn() + + when(fn) + .mockRejectedValue(new Error('default')) + .calledWith('foo').mockRejectedValue(new Error('special')) + + await expect(fn('bar')).rejects.toThrow('default') + await expect(fn('foo')).rejects.toThrow('special') + }) + + it('default reject interoperates with resolve', async () => { + const fn = jest.fn() + + when(fn) + .mockRejectedValue(new Error('non-mocked interaction')) + .calledWith('foo').mockResolvedValue('mocked') + + await expect(fn('foo')).resolves.toEqual('mocked') + await expect(fn('bar')).rejects.toThrow('non-mocked interaction') + }) + + it('can override default', () => { + const fn = jest.fn() + + when(fn) + .mockReturnValue('oldDefault') + .mockReturnValue('newDefault') + .calledWith('foo').mockReturnValue('bar') + + expect(fn('foo')).toEqual('bar') + expect(fn('foo2')).toEqual('newDefault') + }) + + it('allows defining the default NOT in a chained case', async () => { + const fn = jest.fn() + + when(fn).mockRejectedValue(false) + + when(fn) + .calledWith(expect.anything()) + .mockResolvedValue(true) + + await expect(fn('anything')).resolves.toEqual(true) + await expect(fn()).rejects.toEqual(false) + }) + + it('allows overriding the default NOT in a chained case', () => { + const fn = jest.fn() + + when(fn).mockReturnValue(1) + when(fn).mockReturnValue(2) + + when(fn) + .calledWith(expect.anything()) + .mockReturnValue(true) + + expect(fn()).toEqual(2) + }) + }) }) it('will throw because of unintended usage', () => {