Skip to content

Commit 24e2acd

Browse files
committed
feat: create new prefer-to-have-been-called rule
1 parent 281085a commit 24e2acd

File tree

6 files changed

+188
-2
lines changed

6 files changed

+188
-2
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,8 @@ Manually fixable by
378378
| [prefer-strict-equal](docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | | 💡 |
379379
| [prefer-to-be](docs/rules/prefer-to-be.md) | Suggest using `toBe()` for primitive literals | 🎨 | | 🔧 | |
380380
| [prefer-to-contain](docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | 🎨 | | 🔧 | |
381-
| [prefer-to-have-been-called-times](docs/rules/prefer-to-have-been-called-times.md) | Suggest using `toHaveBeenCalledTimes()` | | | 🔧 | |
381+
| [prefer-to-have-been-called](docs/rules/prefer-to-have-been-called.md) | Suggest using `toHaveBeenCalled` | | | 🔧 | |
382+
| [prefer-to-have-been-called-times](docs/rules/prefer-to-have-been-called-times.md) | Suggest using `toHaveBeenCalledTimes()` | | | 🔧 | |
382383
| [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | 🎨 | | 🔧 | |
383384
| [prefer-todo](docs/rules/prefer-todo.md) | Suggest using `test.todo` | | | 🔧 | |
384385
| [require-hook](docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | | | |
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Suggest using `toHaveBeenCalled` (`prefer-to-have-been-called`)
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+
In order to have a better failure message, `toHaveBeenCalled` should be used
9+
instead of checking the number of times a mock has been called.
10+
11+
## Rule details
12+
13+
This rule triggers a warning if `toHaveBeenCalledTimes` is used to assert that a
14+
mock has or has not been called zero times
15+
16+
> [!NOTE]
17+
>
18+
> This rule should ideally be paired with
19+
> [`prefer-to-have-been-called-times`](./prefer-to-have-been-called-times.md)
20+
21+
The following patterns are considered warnings:
22+
23+
```js
24+
expect(someFunction).toHaveBeenCalledTimes(0);
25+
expect(someFunction).not.toHaveBeenCalledTimes(0);
26+
```
27+
28+
The following patterns are not warnings:
29+
30+
```js
31+
expect(someFunction).not.toHaveBeenCalled();
32+
expect(someFunction).toHaveBeenCalled();
33+
```

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
6363
"jest/prefer-strict-equal": "error",
6464
"jest/prefer-to-be": "error",
6565
"jest/prefer-to-contain": "error",
66+
"jest/prefer-to-have-been-called": "error",
6667
"jest/prefer-to-have-been-called-times": "error",
6768
"jest/prefer-to-have-length": "error",
6869
"jest/prefer-todo": "error",
@@ -157,6 +158,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
157158
"jest/prefer-strict-equal": "error",
158159
"jest/prefer-to-be": "error",
159160
"jest/prefer-to-contain": "error",
161+
"jest/prefer-to-have-been-called": "error",
160162
"jest/prefer-to-have-been-called-times": "error",
161163
"jest/prefer-to-have-length": "error",
162164
"jest/prefer-todo": "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 = 65;
5+
const numberOfRules = 66;
66
const ruleNames = Object.keys(plugin.rules);
77
const deprecatedRules = Object.entries(plugin.rules)
88
.filter(([, rule]) => rule.meta.deprecated)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import rule from '../prefer-to-have-been-called';
2+
import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils';
3+
4+
const ruleTester = new RuleTester({
5+
parser: espreeParser,
6+
parserOptions: {
7+
ecmaVersion: 2020,
8+
},
9+
});
10+
11+
ruleTester.run('prefer-to-have-been-called', rule, {
12+
valid: [
13+
'expect(method.mock.calls).toHaveLength;',
14+
'expect(method.mock.calls).toHaveLength(0);',
15+
'expect(method).toHaveBeenCalledTimes(1)',
16+
'expect(method).not.toHaveBeenCalledTimes(x)',
17+
'expect(method).not.toHaveBeenCalledTimes(1)',
18+
'expect(method).not.toHaveBeenCalledTimes(...x)',
19+
'expect(a);',
20+
'expect(method).not.resolves.toHaveBeenCalledTimes(0);',
21+
],
22+
23+
invalid: [
24+
{
25+
code: 'expect(method).toBeCalledTimes(0);',
26+
output: 'expect(method).not.toHaveBeenCalled();',
27+
errors: [{ messageId: 'preferMatcher', column: 16, line: 1 }],
28+
},
29+
{
30+
code: 'expect(method).not.toBeCalledTimes(0);',
31+
output: 'expect(method).toHaveBeenCalled();',
32+
errors: [{ messageId: 'preferMatcher', column: 20, line: 1 }],
33+
},
34+
{
35+
code: 'expect(method).toHaveBeenCalledTimes(0);',
36+
output: 'expect(method).not.toHaveBeenCalled();',
37+
errors: [{ messageId: 'preferMatcher', column: 16, line: 1 }],
38+
},
39+
{
40+
code: 'expect(method).not.toHaveBeenCalledTimes(0);',
41+
output: 'expect(method).toHaveBeenCalled();',
42+
errors: [{ messageId: 'preferMatcher', column: 20, line: 1 }],
43+
},
44+
{
45+
code: 'expect(method).not.toHaveBeenCalledTimes(0, 1, 2);',
46+
output: 'expect(method).toHaveBeenCalled();',
47+
errors: [{ messageId: 'preferMatcher', column: 20, line: 1 }],
48+
},
49+
50+
{
51+
code: 'expect(method).resolves.toHaveBeenCalledTimes(0);',
52+
output: 'expect(method).resolves.not.toHaveBeenCalled();',
53+
errors: [{ messageId: 'preferMatcher', column: 25, line: 1 }],
54+
},
55+
{
56+
code: 'expect(method).rejects.not.toHaveBeenCalledTimes(0);',
57+
output: 'expect(method).rejects.toHaveBeenCalled();',
58+
errors: [{ messageId: 'preferMatcher', column: 28, line: 1 }],
59+
},
60+
61+
{
62+
code: 'expect(method).toBeCalledTimes(0 as number);',
63+
output: 'expect(method).not.toHaveBeenCalled();',
64+
parser: require.resolve('@typescript-eslint/parser'),
65+
errors: [{ messageId: 'preferMatcher', column: 16, line: 1 }],
66+
},
67+
],
68+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2+
import {
3+
createRule,
4+
getAccessorValue,
5+
getFirstMatcherArg,
6+
parseJestFnCall,
7+
} from './utils';
8+
9+
export default createRule({
10+
name: __filename,
11+
meta: {
12+
docs: {
13+
description: 'Suggest using `toHaveBeenCalled`',
14+
},
15+
messages: {
16+
preferMatcher: 'Use `toHaveBeenCalled`',
17+
},
18+
fixable: 'code',
19+
type: 'suggestion',
20+
schema: [],
21+
},
22+
defaultOptions: [],
23+
create(context) {
24+
return {
25+
CallExpression(node) {
26+
const jestFnCall = parseJestFnCall(node, context);
27+
28+
if (jestFnCall?.type !== 'expect') {
29+
return;
30+
}
31+
32+
const { matcher } = jestFnCall;
33+
34+
if (
35+
!['toBeCalledTimes', 'toHaveBeenCalledTimes'].includes(
36+
getAccessorValue(matcher),
37+
)
38+
) {
39+
return;
40+
}
41+
42+
const arg = getFirstMatcherArg(jestFnCall);
43+
44+
if (arg.type !== AST_NODE_TYPES.Literal || arg.value !== 0) {
45+
return;
46+
}
47+
48+
const notModifier = jestFnCall.modifiers.find(
49+
nod => getAccessorValue(nod) === 'not',
50+
);
51+
52+
context.report({
53+
messageId: 'preferMatcher',
54+
node: matcher,
55+
fix(fixer) {
56+
let replacementMatcher = '.not.toHaveBeenCalled';
57+
58+
if (notModifier) {
59+
replacementMatcher = '.toHaveBeenCalled';
60+
}
61+
62+
return [
63+
// remove all the arguments to the matcher
64+
fixer.removeRange([
65+
jestFnCall.args[0].range[0],
66+
jestFnCall.args[jestFnCall.args.length - 1].range[1],
67+
]),
68+
// replace the current matcher with "(.not).toHaveBeenCalled"
69+
fixer.replaceTextRange(
70+
[
71+
(notModifier || matcher).parent.object.range[1],
72+
jestFnCall.matcher.parent.range[1],
73+
],
74+
replacementMatcher,
75+
),
76+
];
77+
},
78+
});
79+
},
80+
};
81+
},
82+
});

0 commit comments

Comments
 (0)