Skip to content

Commit 610b1d4

Browse files
obadakhalilisheremet-va
andauthoredFeb 24, 2023
feat(spy): implement mock.withImplementation API (#2835)
Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>
1 parent 6ff6c6e commit 610b1d4

File tree

4 files changed

+264
-81
lines changed

4 files changed

+264
-81
lines changed
 

‎docs/api/mock.md

+37
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,43 @@ You should use spy assertions (e.g., [`toHaveBeenCalled`](/api/expect#tohavebeen
9191
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn())
9292
```
9393

94+
## withImplementation
95+
96+
- **Type:** `(fn: Function, callback: () => void) => MockInstance`
97+
- **Type:** `(fn: Function, callback: () => Promise<unknown>) => Promise<MockInstance>`
98+
99+
Overrides the original mock implementation temporarily while the callback is being executed.
100+
101+
```js
102+
const myMockFn = vi.fn(() => 'original')
103+
104+
myMockFn.withImplementation(() => 'temp', () => {
105+
myMockFn() // 'temp'
106+
})
107+
108+
myMockFn() // 'original'
109+
```
110+
111+
Can be used with an asynchronous callback. The method has to be awaited to use the original implementation afterward.
112+
113+
```ts
114+
test('async callback', () => {
115+
const myMockFn = vi.fn(() => 'original')
116+
117+
// We await this call since the callback is async
118+
await myMockFn.withImplementation(
119+
() => 'temp',
120+
async () => {
121+
myMockFn() // 'temp'
122+
},
123+
)
124+
125+
myMockFn() // 'original'
126+
})
127+
```
128+
129+
Also, it takes precedence over the [`mockImplementationOnce`](https://vitest.dev/api/mock.html#mockimplementationonce).
130+
94131
## mockRejectedValue
95132

96133
- **Type:** `(value: any) => MockInstance`

‎packages/spy/src/index.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface SpyInstance<TArgs extends any[] = any[], TReturns = any> {
4646
getMockImplementation(): ((...args: TArgs) => TReturns) | undefined
4747
mockImplementation(fn: ((...args: TArgs) => TReturns) | (() => Promise<TReturns>)): this
4848
mockImplementationOnce(fn: ((...args: TArgs) => TReturns) | (() => Promise<TReturns>)): this
49+
withImplementation<T>(fn: ((...args: TArgs) => TReturns), cb: () => T): T extends Promise<unknown> ? Promise<this> : this
4950
mockReturnThis(): this
5051
mockReturnValue(obj: TReturns): this
5152
mockReturnValueOnce(obj: TReturns): this
@@ -208,6 +209,7 @@ function enhanceSpy<TArgs extends any[], TReturns>(
208209
}
209210

210211
let onceImplementations: ((...args: TArgs) => TReturns)[] = []
212+
let implementationChangedTemporarily = false
211213

212214
let name: string = (stub as any).name
213215

@@ -248,6 +250,35 @@ function enhanceSpy<TArgs extends any[], TReturns>(
248250
return stub
249251
}
250252

253+
function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void): EnhancedSpy<TArgs, TReturns>
254+
function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => Promise<void>): Promise<EnhancedSpy<TArgs, TReturns>>
255+
function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void | Promise<void>): EnhancedSpy<TArgs, TReturns> | Promise<EnhancedSpy<TArgs, TReturns>> {
256+
const originalImplementation = implementation
257+
258+
implementation = fn
259+
implementationChangedTemporarily = true
260+
261+
const reset = () => {
262+
implementation = originalImplementation
263+
implementationChangedTemporarily = false
264+
}
265+
266+
const result = cb()
267+
268+
if (result instanceof Promise) {
269+
return result.then(() => {
270+
reset()
271+
return stub
272+
})
273+
}
274+
275+
reset()
276+
277+
return stub
278+
}
279+
280+
stub.withImplementation = withImplementation
281+
251282
stub.mockReturnThis = () =>
252283
stub.mockImplementation(function (this: TReturns) {
253284
return this
@@ -275,7 +306,7 @@ function enhanceSpy<TArgs extends any[], TReturns>(
275306
stub.willCall(function (this: unknown, ...args) {
276307
instances.push(this)
277308
invocations.push(++callOrder)
278-
const impl = onceImplementations.shift() || implementation || stub.getOriginal() || (() => {})
309+
const impl = implementationChangedTemporarily ? implementation! : onceImplementations.shift() || implementation || stub.getOriginal() || (() => {})
279310
return impl.apply(this, args)
280311
})
281312

‎pnpm-lock.yaml

+110-80
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/core/test/mocked.test.ts

+85
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,88 @@ describe('mocked function which fails on toReturnWith', () => {
169169
test('streams', () => {
170170
expect(exportedStream).toBeDefined()
171171
})
172+
173+
describe('temporary mock implementation', () => {
174+
test('temporary mock implementation works as expected', () => {
175+
const mock = vi.fn(() => 1)
176+
177+
expect.assertions(3)
178+
179+
mock.withImplementation(() => 2, () => {
180+
expect(mock()).toBe(2)
181+
expect(mock()).toBe(2)
182+
})
183+
184+
expect(mock()).toBe(1)
185+
})
186+
187+
test('original implementation restored as undefined, when there is none', () => {
188+
const mock = vi.fn()
189+
190+
expect.assertions(5)
191+
192+
mock.withImplementation(() => 2, () => {
193+
expect(mock.getMockImplementation()).toBeTypeOf('function')
194+
expect(mock()).toBe(2)
195+
expect(mock()).toBe(2)
196+
})
197+
198+
expect(mock()).toBe(undefined)
199+
expect(mock.getMockImplementation()).toBe(undefined)
200+
})
201+
202+
test('temporary mock implementation return value can be of different type than the original', async () => {
203+
const mock = vi.fn(() => 1)
204+
205+
expect.assertions(3)
206+
207+
mock.withImplementation(() => 2, () => {
208+
expect(mock()).toBe(2)
209+
expect(mock()).toBe(2)
210+
})
211+
212+
expect(mock()).toBe(1)
213+
})
214+
215+
test('temporary mock implementation with async callback works as expecetd', async () => {
216+
const mock = vi.fn(() => 1)
217+
218+
expect.assertions(3)
219+
220+
await mock.withImplementation(() => 2, async () => {
221+
await Promise.resolve()
222+
223+
expect(mock()).toBe(2)
224+
expect(mock()).toBe(2)
225+
})
226+
227+
expect(mock()).toBe(1)
228+
})
229+
230+
test('temporary mock implementation can be async', async () => {
231+
const mock = vi.fn(async () => 1)
232+
233+
expect.assertions(3)
234+
235+
await mock.withImplementation(async () => 2, async () => {
236+
expect(await mock()).toBe(2)
237+
expect(await mock()).toBe(2)
238+
})
239+
240+
expect(await mock()).toBe(1)
241+
})
242+
243+
test('temporary mock implementation takes precedence over mockImplementationOnce', () => {
244+
const mock = vi.fn(() => 1)
245+
246+
expect.assertions(3)
247+
248+
mock.mockImplementationOnce(() => 2)
249+
mock.withImplementation(() => 3, () => {
250+
expect(mock()).toBe(3)
251+
expect(mock()).toBe(3)
252+
})
253+
254+
expect(mock()).toBe(2)
255+
})
256+
})

0 commit comments

Comments
 (0)
Please sign in to comment.