Skip to content

Commit ce3724f

Browse files
author
Nicolas GILLOT
committed
feat: create new pair-to-have-been-called-assertions rule
1 parent 4851e6b commit ce3724f

File tree

6 files changed

+1096
-65
lines changed

6 files changed

+1096
-65
lines changed

README.md

Lines changed: 65 additions & 64 deletions
Large diffs are not rendered by default.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Require `toHaveBeenCalledTimes()` when using `toHaveBeenCalledWith()` (`pair-to-have-been-called-assertions`)
2+
3+
🔧 This rule is automatically fixable by the
4+
[`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
5+
6+
<!-- end auto-generated rule header -->
7+
8+
## Rule Details
9+
10+
When testing mock functions, developers often use `toHaveBeenCalledWith()`,
11+
`toBeCalledWith()`, `toHaveBeenNthCalledWith()`, `toBeNthCalledWith()`,
12+
`toHaveBeenLastCalledWith()`, or `toBeLastCalledWith()` to verify that a
13+
function was called with specific arguments. However, without also checking the
14+
call count using `toHaveBeenCalledTimes()` or `toBeCalledTimes()`, the test can
15+
pass even if the mock was called more times than expected, potentially masking
16+
bugs.
17+
18+
This rule requires that whenever you use these matchers with arguments, you must
19+
also use the corresponding `toHaveBeenCalledTimes()` or `toBeCalledTimes()`
20+
matcher for the same mock function to ensure an exact call count.
21+
22+
### Benefits
23+
24+
- **Prevents false positives**: Ensures tests fail when a mock is called more
25+
times than expected
26+
- **Makes test intentions explicit**: Clearly documents how many times a
27+
function should be called
28+
- **Improves test reliability**: Catches unexpected behavior where functions are
29+
called multiple times
30+
- **Follows testing best practices**: Encourages complete and precise assertions
31+
32+
## Examples
33+
34+
### Incorrect
35+
36+
```js
37+
expect(mockFn).toHaveBeenCalledWith('arg');
38+
39+
expect(mockFn).toBeCalledWith('arg');
40+
41+
// Multiple assertions without call count check
42+
expect(mockFn).toHaveBeenCalledWith('arg1');
43+
expect(mockFn).toHaveBeenCalledWith('arg2');
44+
45+
// Using toHaveBeenNthCalledWith without call count
46+
expect(mockFn).toHaveBeenNthCalledWith(1, 'first');
47+
48+
// Using toHaveBeenLastCalledWith without call count
49+
expect(mockFn).toHaveBeenLastCalledWith('last');
50+
51+
// Using toBeNthCalledWith without call count
52+
expect(mockFn).toBeNthCalledWith(2, 'second');
53+
54+
// Using toBeLastCalledWith without call count
55+
expect(mockFn).toBeLastCalledWith('last');
56+
```
57+
58+
### Correct
59+
60+
```js
61+
expect(mockFn).toHaveBeenCalledTimes(1);
62+
expect(mockFn).toHaveBeenCalledWith('arg');
63+
64+
expect(mockFn).toBeCalledTimes(1);
65+
expect(mockFn).toBeCalledWith('arg');
66+
67+
// Multiple mocks, each with call count
68+
expect(mockFn1).toHaveBeenCalledTimes(1);
69+
expect(mockFn1).toHaveBeenCalledWith('arg1');
70+
expect(mockFn2).toHaveBeenCalledTimes(1);
71+
expect(mockFn2).toHaveBeenCalledWith('arg2');
72+
73+
// Using toHaveBeenNthCalledWith with call count
74+
expect(mockFn).toHaveBeenCalledTimes(2);
75+
expect(mockFn).toHaveBeenNthCalledWith(1, 'first');
76+
77+
// Using toHaveBeenLastCalledWith with call count
78+
expect(mockFn).toHaveBeenCalledTimes(3);
79+
expect(mockFn).toHaveBeenLastCalledWith('last');
80+
81+
// Using toBeNthCalledWith with call count
82+
expect(mockFn).toBeCalledTimes(2);
83+
expect(mockFn).toBeNthCalledWith(2, 'second');
84+
85+
// Using toBeLastCalledWith with call count
86+
expect(mockFn).toBeCalledTimes(1);
87+
expect(mockFn).toBeLastCalledWith('only');
88+
89+
// Mixed matchers with call count
90+
expect(mockFn).toHaveBeenCalledTimes(3);
91+
expect(mockFn).toHaveBeenCalledWith('arg1');
92+
expect(mockFn).toHaveBeenNthCalledWith(2, 'arg2');
93+
expect(mockFn).toHaveBeenLastCalledWith('arg3');
94+
95+
// Empty call (no arguments) doesn't require call count
96+
expect(mockFn).toHaveBeenCalledWith();
97+
98+
// Using 'not' modifier doesn't require call count
99+
expect(mockFn).not.toHaveBeenCalledWith('arg');
100+
101+
// Only checking call count is fine
102+
expect(mockFn).toHaveBeenCalledTimes(1);
103+
```
104+
105+
## Additional Checks
106+
107+
### Contradictory Assertions
108+
109+
This rule also detects contradictory assertions where `toHaveBeenCalledTimes(0)`
110+
is used together with `toHaveBeenCalledWith()`. Since `toHaveBeenCalledWith()`
111+
expects the mock to be called at least once, using it with
112+
`toHaveBeenCalledTimes(0)` is contradictory.
113+
114+
```js
115+
// ❌ Incorrect - contradictory assertions
116+
test('foo', () => {
117+
expect(mockFn).toHaveBeenCalledTimes(0);
118+
expect(mockFn).toHaveBeenCalledWith('arg'); // This expects a call!
119+
});
120+
121+
// ✅ Correct - consistent assertions
122+
test('foo', () => {
123+
expect(mockFn).toHaveBeenCalledTimes(0);
124+
// No CalledWith assertions
125+
});
126+
127+
// ✅ Correct - consistent assertions
128+
test('foo', () => {
129+
expect(mockFn).toHaveBeenCalledTimes(1);
130+
expect(mockFn).toHaveBeenCalledWith('arg');
131+
});
132+
```
133+
134+
## When Not To Use It
135+
136+
If you have a specific testing strategy where checking call counts is not
137+
necessary or you're only interested in verifying that a function was called with
138+
specific arguments at least once, you can disable this rule.
139+
140+
However, it's generally recommended to keep this rule enabled as it helps catch
141+
bugs where functions are called more times than expected, leading to more robust
142+
and reliable tests.

src/__tests__/__snapshots__/rules.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
4545
"jest/padding-around-describe-blocks": "error",
4646
"jest/padding-around-expect-groups": "error",
4747
"jest/padding-around-test-blocks": "error",
48+
"jest/pair-to-have-been-called-assertions": "error",
4849
"jest/prefer-called-with": "error",
4950
"jest/prefer-comparison-matcher": "error",
5051
"jest/prefer-each": "error",
@@ -137,6 +138,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
137138
"jest/padding-around-describe-blocks": "error",
138139
"jest/padding-around-expect-groups": "error",
139140
"jest/padding-around-test-blocks": "error",
141+
"jest/pair-to-have-been-called-assertions": "error",
140142
"jest/prefer-called-with": "error",
141143
"jest/prefer-comparison-matcher": "error",
142144
"jest/prefer-each": "error",

src/__tests__/rules.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { existsSync } from 'fs';
22
import { resolve } from 'path';
33
import plugin from '../';
44

5-
const numberOfRules = 63;
5+
const numberOfRules = 64;
66
const ruleNames = Object.keys(plugin.rules);
77
const deprecatedRules = Object.entries(plugin.rules)
88
.filter(([, rule]) => rule.meta.deprecated)

0 commit comments

Comments
 (0)