Skip to content

Commit 9ac6268

Browse files
authored
feat(debug): add debug utility (#11)
1 parent d3a51d9 commit 9ac6268

9 files changed

+549
-66
lines changed

README.md

+57-3
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,9 @@ expect(spy()).toBe(undefined)
205205
import type { WhenOptions } from 'vitest-when'
206206
```
207207

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

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

@@ -465,3 +465,57 @@ when(spy)
465465
expect(spy('hello')).toEqual('world')
466466
expect(spy('hello')).toEqual('solar system')
467467
```
468+
469+
### `debug(spy: TFunc, options?: DebugOptions): DebugInfo`
470+
471+
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.
472+
473+
```ts
474+
import { when, debug } from 'vitest-when'
475+
476+
const coolFunc = vi.fn().mockName('coolFunc')
477+
478+
when(coolFunc).calledWith(1, 2, 3).thenReturn(123)
479+
when(coolFunc).calledWith(4, 5, 6).thenThrow(new Error('oh no'))
480+
481+
const result = coolFunc(1, 2, 4)
482+
483+
debug(coolFunc)
484+
// `coolFunc()` has:
485+
// * 2 stubbings with 0 calls
486+
// * Called 0 times: `(1, 2, 3) => 123`
487+
// * Called 0 times: `(4, 5, 6) => { throw [Error: oh no] }`
488+
// * 1 unmatched call
489+
// * `(1, 2, 4)`
490+
```
491+
492+
#### `DebugOptions`
493+
494+
```ts
495+
import type { DebugOptions } from 'vitest-when'
496+
```
497+
498+
| option | default | type | description |
499+
| ------ | ------- | ------- | -------------------------------------- |
500+
| `log` | `true` | boolean | Whether the call to `debug` should log |
501+
502+
#### `DebugResult`
503+
504+
```ts
505+
import type { DebugResult, Stubbing, Behavior } from 'vitest-when'
506+
```
507+
508+
| fields | type | description |
509+
| ---------------------------- | -------------------------------------------- | ----------------------------------------------------------- |
510+
| `description` | `string` | A human-readable description of the stub, logged by default |
511+
| `name` | `string` | The name of the mock, if set by [`mockName`][mockName] |
512+
| `stubbings` | `Stubbing[]` | The list of configured stub behaviors |
513+
| `stubbings[].args` | `unknown[]` | The stubbing's arguments to match |
514+
| `stubbings[].behavior` | `Behavior` | The configured behavior of the stubbing |
515+
| `stubbings[].behavior.type` | `return`, `throw`, `resolve`, `reject`, `do` | Result type of the stubbing |
516+
| `stubbings[].behavior.value` | `unknown` | Value for the behavior, if `type` is `return` or `resolve` |
517+
| `stubbings[].behavior.error` | `unknown` | Error for the behavior, it `type` is `throw` or `reject` |
518+
| `stubbings[].matchedCalls` | `unknown[][]` | Actual calls that matched the stubbing, if any |
519+
| `unmatchedCalls` | `unknown[][]` | Actual calls that did not match a stubbing |
520+
521+
[mockName]: https://vitest.dev/api/mock.html#mockname

example/meaning-of-life.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { vi, describe, afterEach, it, expect } from 'vitest'
2-
import { when } from 'vitest-when'
2+
import { when, debug } from 'vitest-when'
33

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

2020
const result = await subject.createMeaning()
2121

22+
debug(deepThought.calculateAnswer)
23+
debug(earth.calculateQuestion)
24+
2225
expect(result).toEqual({ question: "What's 6 by 9?", answer: 42 })
2326
})
2427
})

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,8 @@
8181
"publishConfig": {
8282
"access": "public",
8383
"provenance": true
84+
},
85+
"dependencies": {
86+
"pretty-format": "^29.7.0"
8487
}
8588
}

pnpm-lock.yaml

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

src/behaviors.ts

+77-35
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export interface WhenOptions {
88
export interface BehaviorStack<TFunc extends AnyFunction> {
99
use: (args: Parameters<TFunc>) => BehaviorEntry<Parameters<TFunc>> | undefined
1010

11+
getAll: () => readonly BehaviorEntry<Parameters<TFunc>>[]
12+
13+
getUnmatchedCalls: () => readonly Parameters<TFunc>[]
14+
1115
bindArgs: <TArgs extends Parameters<TFunc>>(
1216
args: TArgs,
1317
options: WhenOptions,
@@ -24,80 +28,115 @@ export interface BoundBehaviorStack<TReturn> {
2428

2529
export interface BehaviorEntry<TArgs extends unknown[]> {
2630
args: TArgs
27-
returnValue?: unknown
28-
rejectError?: unknown
29-
throwError?: unknown
30-
doCallback?: AnyFunction | undefined
31-
times?: number | undefined
31+
behavior: Behavior
32+
calls: TArgs[]
33+
maxCallCount?: number | undefined
3234
}
3335

36+
export const BehaviorType = {
37+
RETURN: 'return',
38+
RESOLVE: 'resolve',
39+
THROW: 'throw',
40+
REJECT: 'reject',
41+
DO: 'do',
42+
} as const
43+
44+
export type Behavior =
45+
| { type: typeof BehaviorType.RETURN; value: unknown }
46+
| { type: typeof BehaviorType.RESOLVE; value: unknown }
47+
| { type: typeof BehaviorType.THROW; error: unknown }
48+
| { type: typeof BehaviorType.REJECT; error: unknown }
49+
| { type: typeof BehaviorType.DO; callback: AnyFunction }
50+
3451
export interface BehaviorOptions<TValue> {
3552
value: TValue
36-
times: number | undefined
53+
maxCallCount: number | undefined
3754
}
3855

3956
export const createBehaviorStack = <
4057
TFunc extends AnyFunction,
4158
>(): BehaviorStack<TFunc> => {
4259
const behaviors: BehaviorEntry<Parameters<TFunc>>[] = []
60+
const unmatchedCalls: Parameters<TFunc>[] = []
4361

4462
return {
63+
getAll: () => behaviors,
64+
65+
getUnmatchedCalls: () => unmatchedCalls,
66+
4567
use: (args) => {
4668
const behavior = behaviors
4769
.filter((b) => behaviorAvailable(b))
4870
.find(behaviorMatches(args))
4971

50-
if (behavior?.times !== undefined) {
51-
behavior.times -= 1
72+
if (!behavior) {
73+
unmatchedCalls.push(args)
74+
return undefined
5275
}
5376

77+
behavior.calls.push(args)
5478
return behavior
5579
},
5680

5781
bindArgs: (args, options) => ({
5882
addReturn: (values) => {
5983
behaviors.unshift(
60-
...getBehaviorOptions(values, options).map(({ value, times }) => ({
61-
args,
62-
times,
63-
returnValue: value,
64-
})),
84+
...getBehaviorOptions(values, options).map(
85+
({ value, maxCallCount }) => ({
86+
args,
87+
maxCallCount,
88+
behavior: { type: BehaviorType.RETURN, value },
89+
calls: [],
90+
}),
91+
),
6592
)
6693
},
6794
addResolve: (values) => {
6895
behaviors.unshift(
69-
...getBehaviorOptions(values, options).map(({ value, times }) => ({
70-
args,
71-
times,
72-
returnValue: Promise.resolve(value),
73-
})),
96+
...getBehaviorOptions(values, options).map(
97+
({ value, maxCallCount }) => ({
98+
args,
99+
maxCallCount,
100+
behavior: { type: BehaviorType.RESOLVE, value },
101+
calls: [],
102+
}),
103+
),
74104
)
75105
},
76106
addThrow: (values) => {
77107
behaviors.unshift(
78-
...getBehaviorOptions(values, options).map(({ value, times }) => ({
79-
args,
80-
times,
81-
throwError: value,
82-
})),
108+
...getBehaviorOptions(values, options).map(
109+
({ value, maxCallCount }) => ({
110+
args,
111+
maxCallCount,
112+
behavior: { type: BehaviorType.THROW, error: value },
113+
calls: [],
114+
}),
115+
),
83116
)
84117
},
85118
addReject: (values) => {
86119
behaviors.unshift(
87-
...getBehaviorOptions(values, options).map(({ value, times }) => ({
88-
args,
89-
times,
90-
rejectError: value,
91-
})),
120+
...getBehaviorOptions(values, options).map(
121+
({ value, maxCallCount }) => ({
122+
args,
123+
maxCallCount,
124+
behavior: { type: BehaviorType.REJECT, error: value },
125+
calls: [],
126+
}),
127+
),
92128
)
93129
},
94130
addDo: (values) => {
95131
behaviors.unshift(
96-
...getBehaviorOptions(values, options).map(({ value, times }) => ({
97-
args,
98-
times,
99-
doCallback: value,
100-
})),
132+
...getBehaviorOptions(values, options).map(
133+
({ value, maxCallCount }) => ({
134+
args,
135+
maxCallCount,
136+
behavior: { type: BehaviorType.DO, callback: value },
137+
calls: [],
138+
}),
139+
),
101140
)
102141
},
103142
}),
@@ -114,14 +153,17 @@ const getBehaviorOptions = <TValue>(
114153

115154
return values.map((value, index) => ({
116155
value,
117-
times: times ?? (index < values.length - 1 ? 1 : undefined),
156+
maxCallCount: times ?? (index < values.length - 1 ? 1 : undefined),
118157
}))
119158
}
120159

121160
const behaviorAvailable = <TArgs extends unknown[]>(
122161
behavior: BehaviorEntry<TArgs>,
123162
): boolean => {
124-
return behavior.times === undefined || behavior.times > 0
163+
return (
164+
behavior.maxCallCount === undefined ||
165+
behavior.calls.length < behavior.maxCallCount
166+
)
125167
}
126168

127169
const behaviorMatches = <TArgs extends unknown[]>(args: TArgs) => {

0 commit comments

Comments
 (0)