Skip to content

Commit

Permalink
feat: fallback to original mockImplementation if no match (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcous authored Nov 4, 2024
1 parent 46fb4cb commit 5cd3283
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 14 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export const calculateQuestion = async (answer: number): Promise<string> => {

### `when(spy: TFunc, options?: WhenOptions): StubWrapper<TFunc>`

Configures a `vi.fn()` mock function to act as a vitest-when stub. Adds an implementation to the function that initially no-ops, and returns an API to configure behaviors for given arguments using [`.calledWith(...)`][called-with]
Configures a `vi.fn()` or `vi.spyOn()` mock function to act as a vitest-when stub. Adds an implementation to the function that initially no-ops, and returns an API to configure behaviors for given arguments using [`.calledWith(...)`][called-with]

```ts
import { vi } from 'vitest'
Expand Down Expand Up @@ -264,6 +264,21 @@ when(overloaded).calledWith().thenReturn(null)
when<() => null>(overloaded).calledWith().thenReturn(null)
```

#### Fallback

By default, if arguments do not match, a vitest-when stub will no-op and return `undefined`. You can customize this fallback by configuring your own unconditional behavior on the mock using Vitest's built-in [mock API][].

```ts
const spy = vi.fn().mockReturnValue('you messed up!')

when(spy).calledWith('hello').thenReturn('world')

spy('hello') // "world"
spy('jello') // "you messed up!"
```

[mock API]: https://vitest.dev/api/mock.html

### `.thenReturn(value: TReturn)`

When the stubbing is satisfied, return `value`
Expand Down
8 changes: 4 additions & 4 deletions src/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
} from 'pretty-format'

import { validateSpy, getBehaviorStack } from './stubs'
import type { AnyFunction } from './types'
import type { AnyFunction, MockInstance } from './types'
import { type Behavior, BehaviorType } from './behaviors'

export interface DebugResult {
Expand All @@ -21,11 +21,11 @@ export interface Stubbing {
}

export const getDebug = <TFunc extends AnyFunction>(
spy: TFunc,
spy: TFunc | MockInstance<TFunc>,
): DebugResult => {
const target = validateSpy(spy)
const target = validateSpy<TFunc>(spy)
const name = target.getMockName()
const behaviors = getBehaviorStack<TFunc>(target)
const behaviors = getBehaviorStack(target)
const unmatchedCalls = behaviors?.getUnmatchedCalls() ?? target.mock.calls
const stubbings =
behaviors?.getAll().map((entry) => ({
Expand Down
19 changes: 11 additions & 8 deletions src/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,20 @@ interface WhenStubImplementation<TFunc extends AnyFunction> {
export const configureStub = <TFunc extends AnyFunction>(
maybeSpy: unknown,
): BehaviorStack<TFunc> => {
const spy = validateSpy(maybeSpy)
const spy = validateSpy<TFunc>(maybeSpy)
const existingBehaviors = getBehaviorStack(spy)

if (existingBehaviors) {
return existingBehaviors
}

const behaviors = createBehaviorStack<TFunc>()
const fallbackImplementation = spy.getMockImplementation()

const implementation = (...args: Parameters<TFunc>) => {
const behavior = behaviors.use(args)?.behavior ?? {
type: BehaviorType.RETURN,
value: undefined,
type: BehaviorType.DO,
callback: fallbackImplementation,
}

switch (behavior.type) {
Expand All @@ -50,19 +51,21 @@ export const configureStub = <TFunc extends AnyFunction>(
}

case BehaviorType.DO: {
return behavior.callback(...args)
return behavior.callback?.(...args)
}
}
}

spy.mockImplementation(
Object.assign(implementation, { [BEHAVIORS_KEY]: behaviors }),
Object.assign(implementation as TFunc, { [BEHAVIORS_KEY]: behaviors }),
)

return behaviors
}

export const validateSpy = (maybeSpy: unknown): MockInstance => {
export const validateSpy = <TFunc extends AnyFunction>(
maybeSpy: unknown,
): MockInstance<TFunc> => {
if (
typeof maybeSpy === 'function' &&
'mockImplementation' in maybeSpy &&
Expand All @@ -72,14 +75,14 @@ export const validateSpy = (maybeSpy: unknown): MockInstance => {
'getMockName' in maybeSpy &&
typeof maybeSpy.getMockName === 'function'
) {
return maybeSpy as unknown as MockInstance
return maybeSpy as unknown as MockInstance<TFunc>
}

throw new NotAMockFunctionError(maybeSpy)
}

export const getBehaviorStack = <TFunc extends AnyFunction>(
spy: MockInstance,
spy: MockInstance<TFunc>,
): BehaviorStack<TFunc> | undefined => {
const existingImplementation = spy.getMockImplementation() as
| WhenStubImplementation<TFunc>
Expand Down
2 changes: 1 addition & 1 deletion src/vitest-when.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface DebugOptions {
}

export const debug = <TFunc extends AnyFunction>(
spy: TFunc,
spy: TFunc | MockInstance<TFunc>,
options: DebugOptions = {},
): DebugResult => {
const log = options.log ?? true
Expand Down
9 changes: 9 additions & 0 deletions test/vitest-when.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ describe('vitest-when', () => {
expect(spy(1, 2, 3)).toEqual(undefined)
})

it('should fall back to original mock implementation', () => {
const spy = vi.fn().mockReturnValue(100)

subject.when(spy).calledWith(1, 2, 3).thenReturn(4)

expect(spy(1, 2, 3)).toEqual(4)
expect(spy()).toEqual(100)
})

it('should return a number of times', () => {
const spy = vi.fn()

Expand Down

0 comments on commit 5cd3283

Please sign in to comment.