Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fallback to original mockImplementation if no match #17

Merged
merged 1 commit into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading