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

Add function that verifies that all when mocks are called #10

Closed
Closed
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ test('stubbing with vitest-when', () => {

You should call `vi.resetAllMocks()` in your suite's `afterEach` hook to remove the implementation added by `when`. You can also set Vitest's [`mockReset`](https://vitest.dev/config/#mockreset) config to `true` instead of using `afterEach`.

#### Supports verifying that all mocked functions were called

Call `verifyAllWhenMocksCalled` after your test to assert that all mocks using `{ times: x }` were used.

```javascript
import { verifyAllWhenMocksCalled, when } from 'vitest-when'

afterEach(() => {
verifyAllWhenMocksCalled() // fails because spy is only called once
})

it('should call spy two times', () => {
const spy = vi.fn()
when(spy, {times: 2}).calledWith(1, 2, 3).thenReturn(4)
spy(1, 2, 3)
})
```


[vitest's mock functions]: https://vitest.dev/api/mock.html
[stubs]: https://en.wikipedia.org/wiki/Test_stub
[when]: #whenspy-tfunc-stubwrappertfunc
Expand Down
4 changes: 4 additions & 0 deletions src/behaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface WhenOptions {
export interface BehaviorStack<TFunc extends AnyFunction> {
use: (args: Parameters<TFunc>) => BehaviorEntry<Parameters<TFunc>> | undefined

get: () => BehaviorEntry<Parameters<TFunc>>[]

bindArgs: <TArgs extends Parameters<TFunc>>(
args: TArgs,
options: WhenOptions,
Expand Down Expand Up @@ -54,6 +56,8 @@ export const createBehaviorStack = <
return behavior
},

get: () => behaviors,

bindArgs: (args, options) => ({
addReturn: (values) => {
behaviors.unshift(
Expand Down
14 changes: 13 additions & 1 deletion src/vitest-when.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { assert } from 'vitest'
import { configureStub } from './stubs.ts'
import type { WhenOptions } from './behaviors.ts'
import type { BehaviorStack, WhenOptions } from './behaviors.ts'
import type { AnyFunction } from './types.ts'

export type { WhenOptions } from './behaviors.ts'
export * from './errors.ts'

export const behaviorStackRegistry = new Set<BehaviorStack<AnyFunction>>()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces global state into the library, which requires cleanup. I don't think I want that added complexity in this library. I think it would be confusing to require both vi.resetAllMocks and some other resetAllWhenMocks


export interface StubWrapper<TFunc extends AnyFunction> {
calledWith<TArgs extends Parameters<TFunc>>(
...args: TArgs
Expand All @@ -25,6 +28,8 @@ export const when = <TFunc extends AnyFunction>(
): StubWrapper<TFunc> => {
const behaviorStack = configureStub(spy)

behaviorStackRegistry.add(behaviorStack)

return {
calledWith: (...args) => {
const behaviors = behaviorStack.bindArgs(args, options)
Expand All @@ -39,3 +44,10 @@ export const when = <TFunc extends AnyFunction>(
},
}
}

export const verifyAllWhenMocksCalled = () => {
const uncalledMocks = [...behaviorStackRegistry].flatMap((behaviorStack) => {
return behaviorStack.get().filter(behavior => behavior.times && behavior.times > 0)
})
assert.equal(uncalledMocks.length,0, `Failed verifyAllWhenMocksCalled: ${uncalledMocks.length} mock(s) not called:`)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this assertion message is very helpful, and I think that shows a technical limitation to trying to include this kind of functionality inside of this library. I also think it miscommunicates to the user a little; there is a 1 to many relationship between vi.fn() mocks and entries in the behavior stack

}
21 changes: 20 additions & 1 deletion test/vitest-when.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { vi, describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'

import * as subject from '../src/vitest-when.ts'
import AssertionError = Chai.AssertionError

declare module 'vitest' {
interface AsymmetricMatchersContaining {
Expand Down Expand Up @@ -264,4 +265,22 @@ describe('vitest-when', () => {
// intentionally do not call the spy
expect(true).toBe(true)
})

it('should fail if there are uncalled mocks and verify function is called', () => {
const spy = vi.fn()
subject.when(spy, { times: 2 }).calledWith(1, 2, 3).thenReturn(4)
spy(1, 2, 3)

let error: AssertionError | undefined = undefined
try {
subject.verifyAllWhenMocksCalled()
} catch (assertionError) {
error = assertionError as AssertionError
expect(error).toEqual(expect.objectContaining({ expected: 0, actual: 1 }))
expect(error.message).toContain(
'Failed verifyAllWhenMocksCalled: 1 mock(s) not called',
)
}
expect(error).toBeDefined()
})
})
Loading