Skip to content

Commit ff66206

Browse files
jacoberdman2147sheremet-va
andauthoredNov 13, 2024
feat(expect): add toHaveBeenCalledExactlyOnceWith expect matcher (#6894)
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
1 parent 391860f commit ff66206

File tree

4 files changed

+118
-0
lines changed

4 files changed

+118
-0
lines changed
 

‎docs/api/expect.md

+24
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,30 @@ test('spy function', () => {
876876
})
877877
```
878878

879+
## toHaveBeenCalledExactlyOnceWith <Version>2.2.0</Version> {#tohavebeencalledexactlyoncewith}
880+
881+
- **Type**: `(...args: any[]) => Awaitable<void>`
882+
883+
This assertion checks if a function was called exactly once and with certain parameters. Requires a spy function to be passed to `expect`.
884+
885+
```ts
886+
import { expect, test, vi } from 'vitest'
887+
888+
const market = {
889+
buy(subject: string, amount: number) {
890+
// ...
891+
},
892+
}
893+
894+
test('spy function', () => {
895+
const buySpy = vi.spyOn(market, 'buy')
896+
897+
market.buy('apples', 10)
898+
899+
expect(buySpy).toHaveBeenCalledExactlyOnceWith('apples', 10)
900+
})
901+
```
902+
879903
## toHaveBeenLastCalledWith
880904

881905
- **Type**: `(...args: any[]) => Awaitable<void>`

‎packages/expect/src/jest-expect.ts

+21
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,27 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
595595
throw new AssertionError(formatCalls(spy, msg, args))
596596
}
597597
})
598+
def('toHaveBeenCalledExactlyOnceWith', function (...args) {
599+
const spy = getSpy(this)
600+
const spyName = spy.getMockName()
601+
const callCount = spy.mock.calls.length
602+
const hasCallWithArgs = spy.mock.calls.some(callArg =>
603+
jestEquals(callArg, args, [...customTesters, iterableEquality]),
604+
)
605+
const pass = hasCallWithArgs && callCount === 1
606+
const isNot = utils.flag(this, 'negate') as boolean
607+
608+
const msg = utils.getMessage(this, [
609+
pass,
610+
`expected "${spyName}" to be called once with arguments: #{exp}`,
611+
`expected "${spyName}" to not be called once with arguments: #{exp}`,
612+
args,
613+
])
614+
615+
if ((pass && isNot) || (!pass && !isNot)) {
616+
throw new AssertionError(formatCalls(spy, msg, args))
617+
}
618+
})
598619
def(
599620
['toHaveBeenNthCalledWith', 'nthCalledWith'],
600621
function (times: number, ...args: any[]) {

‎packages/expect/src/types.ts

+9
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,15 @@ export interface Assertion<T = any>
635635
*/
636636
toHaveBeenCalledOnce: () => void
637637

638+
/**
639+
* Ensure that a mock function is called with specific arguments and called
640+
* exactly once.
641+
*
642+
* @example
643+
* expect(mockFunc).toHaveBeenCalledExactlyOnceWith('arg1', 42);
644+
*/
645+
toHaveBeenCalledExactlyOnceWith: <E extends any[]>(...args: E) => void
646+
638647
/**
639648
* Checks that a value satisfies a custom matcher function.
640649
*

‎test/core/test/jest-expect.test.ts

+64
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,70 @@ describe('toHaveBeenCalledWith', () => {
634634
})
635635
})
636636

637+
describe('toHaveBeenCalledExactlyOnceWith', () => {
638+
describe('negated', () => {
639+
it('fails if called', () => {
640+
const mock = vi.fn()
641+
mock(3)
642+
643+
expect(() => {
644+
expect(mock).not.toHaveBeenCalledExactlyOnceWith(3)
645+
}).toThrow(/^expected "spy" to not be called once with arguments: \[ 3 \][^e]/)
646+
})
647+
648+
it('passes if called multiple times with args', () => {
649+
const mock = vi.fn()
650+
mock(3)
651+
mock(3)
652+
653+
expect(mock).not.toHaveBeenCalledExactlyOnceWith(3)
654+
})
655+
656+
it('passes if not called', () => {
657+
const mock = vi.fn()
658+
expect(mock).not.toHaveBeenCalledExactlyOnceWith(3)
659+
})
660+
661+
it('passes if called with a different argument', () => {
662+
const mock = vi.fn()
663+
mock(4)
664+
665+
expect(mock).not.toHaveBeenCalledExactlyOnceWith(3)
666+
})
667+
})
668+
669+
it('fails if not called or called too many times', () => {
670+
const mock = vi.fn()
671+
672+
expect(() => {
673+
expect(mock).toHaveBeenCalledExactlyOnceWith(3)
674+
}).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/)
675+
676+
mock(3)
677+
mock(3)
678+
679+
expect(() => {
680+
expect(mock).toHaveBeenCalledExactlyOnceWith(3)
681+
}).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/)
682+
})
683+
684+
it('fails if called with wrong args', () => {
685+
const mock = vi.fn()
686+
mock(4)
687+
688+
expect(() => {
689+
expect(mock).toHaveBeenCalledExactlyOnceWith(3)
690+
}).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/)
691+
})
692+
693+
it('passes if called exactly once with args', () => {
694+
const mock = vi.fn()
695+
mock(3)
696+
697+
expect(mock).toHaveBeenCalledExactlyOnceWith(3)
698+
})
699+
})
700+
637701
describe('async expect', () => {
638702
it('resolves', async () => {
639703
await expect((async () => 'true')()).resolves.toBe('true')

0 commit comments

Comments
 (0)
Please sign in to comment.