Skip to content

Commit

Permalink
feat(debug): add debug utility (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcous authored May 11, 2024
1 parent d3a51d9 commit 9ac6268
Show file tree
Hide file tree
Showing 9 changed files with 549 additions and 66 deletions.
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

0 comments on commit 9ac6268

Please sign in to comment.