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(debug): add debug utility #11

Merged
merged 6 commits into from
May 11, 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
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,9 @@ expect(spy()).toBe(undefined)
import type { WhenOptions } from 'vitest-when'
```

| option | required | type | description |
| ------- | -------- | ------- | -------------------------------------------------- |
| `times` | no | integer | Only trigger configured behavior a number of times |
| option | default | type | description |
| ------- | ------- | ------- | -------------------------------------------------- |
| `times` | N/A | integer | Only trigger configured behavior a number of times |

### `.calledWith(...args: TArgs): Stub<TArgs, TReturn>`

Expand Down Expand Up @@ -465,3 +465,57 @@ when(spy)
expect(spy('hello')).toEqual('world')
expect(spy('hello')).toEqual('solar system')
```

### `debug(spy: TFunc, options?: DebugOptions): DebugInfo`

Logs and returns information about a mock's stubbing and usage. Useful if a test with mocks is failing and you can't figure out why.

```ts
import { when, debug } from 'vitest-when'

const coolFunc = vi.fn().mockName('coolFunc')

when(coolFunc).calledWith(1, 2, 3).thenReturn(123)
when(coolFunc).calledWith(4, 5, 6).thenThrow(new Error('oh no'))

const result = coolFunc(1, 2, 4)

debug(coolFunc)
// `coolFunc()` has:
// * 2 stubbings with 0 calls
// * Called 0 times: `(1, 2, 3) => 123`
// * Called 0 times: `(4, 5, 6) => { throw [Error: oh no] }`
// * 1 unmatched call
// * `(1, 2, 4)`
```

#### `DebugOptions`

```ts
import type { DebugOptions } from 'vitest-when'
```

| option | default | type | description |
| ------ | ------- | ------- | -------------------------------------- |
| `log` | `true` | boolean | Whether the call to `debug` should log |

#### `DebugResult`

```ts
import type { DebugResult, Stubbing, Behavior } from 'vitest-when'
```

| fields | type | description |
| ---------------------------- | -------------------------------------------- | ----------------------------------------------------------- |
| `description` | `string` | A human-readable description of the stub, logged by default |
| `name` | `string` | The name of the mock, if set by [`mockName`][mockName] |
| `stubbings` | `Stubbing[]` | The list of configured stub behaviors |
| `stubbings[].args` | `unknown[]` | The stubbing's arguments to match |
| `stubbings[].behavior` | `Behavior` | The configured behavior of the stubbing |
| `stubbings[].behavior.type` | `return`, `throw`, `resolve`, `reject`, `do` | Result type of the stubbing |
| `stubbings[].behavior.value` | `unknown` | Value for the behavior, if `type` is `return` or `resolve` |
| `stubbings[].behavior.error` | `unknown` | Error for the behavior, it `type` is `throw` or `reject` |
| `stubbings[].matchedCalls` | `unknown[][]` | Actual calls that matched the stubbing, if any |
| `unmatchedCalls` | `unknown[][]` | Actual calls that did not match a stubbing |

[mockName]: https://vitest.dev/api/mock.html#mockname
5 changes: 4 additions & 1 deletion example/meaning-of-life.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { vi, describe, afterEach, it, expect } from 'vitest'
import { when } from 'vitest-when'
import { when, debug } from 'vitest-when'

import * as deepThought from './deep-thought.ts'
import * as earth from './earth.ts'
Expand All @@ -19,6 +19,9 @@ describe('get the meaning of life', () => {

const result = await subject.createMeaning()

debug(deepThought.calculateAnswer)
debug(earth.calculateQuestion)

expect(result).toEqual({ question: "What's 6 by 9?", answer: 42 })
})
})
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,8 @@
"publishConfig": {
"access": "public",
"provenance": true
},
"dependencies": {
"pretty-format": "^29.7.0"
}
}
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 77 additions & 35 deletions src/behaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export interface WhenOptions {
export interface BehaviorStack<TFunc extends AnyFunction> {
use: (args: Parameters<TFunc>) => BehaviorEntry<Parameters<TFunc>> | undefined

getAll: () => readonly BehaviorEntry<Parameters<TFunc>>[]

getUnmatchedCalls: () => readonly Parameters<TFunc>[]

bindArgs: <TArgs extends Parameters<TFunc>>(
args: TArgs,
options: WhenOptions,
Expand All @@ -24,80 +28,115 @@ export interface BoundBehaviorStack<TReturn> {

export interface BehaviorEntry<TArgs extends unknown[]> {
args: TArgs
returnValue?: unknown
rejectError?: unknown
throwError?: unknown
doCallback?: AnyFunction | undefined
times?: number | undefined
behavior: Behavior
calls: TArgs[]
maxCallCount?: number | undefined
}

export const BehaviorType = {
RETURN: 'return',
RESOLVE: 'resolve',
THROW: 'throw',
REJECT: 'reject',
DO: 'do',
} as const

export type Behavior =
| { type: typeof BehaviorType.RETURN; value: unknown }
| { type: typeof BehaviorType.RESOLVE; value: unknown }
| { type: typeof BehaviorType.THROW; error: unknown }
| { type: typeof BehaviorType.REJECT; error: unknown }
| { type: typeof BehaviorType.DO; callback: AnyFunction }

export interface BehaviorOptions<TValue> {
value: TValue
times: number | undefined
maxCallCount: number | undefined
}

export const createBehaviorStack = <
TFunc extends AnyFunction,
>(): BehaviorStack<TFunc> => {
const behaviors: BehaviorEntry<Parameters<TFunc>>[] = []
const unmatchedCalls: Parameters<TFunc>[] = []

return {
getAll: () => behaviors,

getUnmatchedCalls: () => unmatchedCalls,

use: (args) => {
const behavior = behaviors
.filter((b) => behaviorAvailable(b))
.find(behaviorMatches(args))

if (behavior?.times !== undefined) {
behavior.times -= 1
if (!behavior) {
unmatchedCalls.push(args)
return undefined
}

behavior.calls.push(args)
return behavior
},

bindArgs: (args, options) => ({
addReturn: (values) => {
behaviors.unshift(
...getBehaviorOptions(values, options).map(({ value, times }) => ({
args,
times,
returnValue: value,
})),
...getBehaviorOptions(values, options).map(
({ value, maxCallCount }) => ({
args,
maxCallCount,
behavior: { type: BehaviorType.RETURN, value },
calls: [],
}),
),
)
},
addResolve: (values) => {
behaviors.unshift(
...getBehaviorOptions(values, options).map(({ value, times }) => ({
args,
times,
returnValue: Promise.resolve(value),
})),
...getBehaviorOptions(values, options).map(
({ value, maxCallCount }) => ({
args,
maxCallCount,
behavior: { type: BehaviorType.RESOLVE, value },
calls: [],
}),
),
)
},
addThrow: (values) => {
behaviors.unshift(
...getBehaviorOptions(values, options).map(({ value, times }) => ({
args,
times,
throwError: value,
})),
...getBehaviorOptions(values, options).map(
({ value, maxCallCount }) => ({
args,
maxCallCount,
behavior: { type: BehaviorType.THROW, error: value },
calls: [],
}),
),
)
},
addReject: (values) => {
behaviors.unshift(
...getBehaviorOptions(values, options).map(({ value, times }) => ({
args,
times,
rejectError: value,
})),
...getBehaviorOptions(values, options).map(
({ value, maxCallCount }) => ({
args,
maxCallCount,
behavior: { type: BehaviorType.REJECT, error: value },
calls: [],
}),
),
)
},
addDo: (values) => {
behaviors.unshift(
...getBehaviorOptions(values, options).map(({ value, times }) => ({
args,
times,
doCallback: value,
})),
...getBehaviorOptions(values, options).map(
({ value, maxCallCount }) => ({
args,
maxCallCount,
behavior: { type: BehaviorType.DO, callback: value },
calls: [],
}),
),
)
},
}),
Expand All @@ -114,14 +153,17 @@ const getBehaviorOptions = <TValue>(

return values.map((value, index) => ({
value,
times: times ?? (index < values.length - 1 ? 1 : undefined),
maxCallCount: times ?? (index < values.length - 1 ? 1 : undefined),
}))
}

const behaviorAvailable = <TArgs extends unknown[]>(
behavior: BehaviorEntry<TArgs>,
): boolean => {
return behavior.times === undefined || behavior.times > 0
return (
behavior.maxCallCount === undefined ||
behavior.calls.length < behavior.maxCallCount
)
}

const behaviorMatches = <TArgs extends unknown[]>(args: TArgs) => {
Expand Down
Loading
Loading