Skip to content

Commit b8e61b1

Browse files
authored
feat: create no-restricted-jest-methods rule (#1257)
1 parent dbd072a commit b8e61b1

File tree

6 files changed

+227
-1
lines changed

6 files changed

+227
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ installations requiring long-term consistency.
220220
| [no-jasmine-globals](docs/rules/no-jasmine-globals.md) | Disallow Jasmine globals | ![recommended][] | ![fixable][] |
221221
| [no-large-snapshots](docs/rules/no-large-snapshots.md) | disallow large snapshots | | |
222222
| [no-mocks-import](docs/rules/no-mocks-import.md) | Disallow manually importing from `__mocks__` | ![recommended][] | |
223+
| [no-restricted-jest-methods](docs/rules/no-restricted-jest-methods.md) | Disallow specific `jest.` methods | | |
223224
| [no-restricted-matchers](docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | |
224225
| [no-standalone-expect](docs/rules/no-standalone-expect.md) | Disallow using `expect` outside of `it` or `test` blocks | ![recommended][] | |
225226
| [no-test-prefixes](docs/rules/no-test-prefixes.md) | Use `.only` and `.skip` over `f` and `x` | ![recommended][] | ![fixable][] |
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Disallow specific `jest.` methods (`no-restricted-jest-methods`)
2+
3+
💼 This rule is enabled in the following
4+
[configs](https://github.com/jest-community/eslint-plugin-jest/blob/main/README.md#shareable-configurations):
5+
`all`.
6+
7+
<!-- end rule header -->
8+
9+
You may wish to restrict the use of specific `jest` methods.
10+
11+
## Rule details
12+
13+
This rule checks for the usage of specific methods on the `jest` object, which
14+
can be used to disallow curtain patterns such as spies and mocks.
15+
16+
## Options
17+
18+
Restrictions are expressed in the form of a map, with the value being either a
19+
string message to be shown, or `null` if a generic default message should be
20+
used.
21+
22+
By default, this map is empty, meaning no `jest` methods are banned.
23+
24+
For example:
25+
26+
```json
27+
{
28+
"jest/no-restricted-jest-methods": [
29+
"error",
30+
{
31+
"advanceTimersByTime": null,
32+
"spyOn": "Don't use spies"
33+
}
34+
]
35+
}
36+
```
37+
38+
Examples of **incorrect** code for this rule with the above configuration
39+
40+
```js
41+
jest.useFakeTimers();
42+
it('calls the callback after 1 second via advanceTimersByTime', () => {
43+
// ...
44+
45+
jest.advanceTimersByTime(1000);
46+
47+
// ...
48+
});
49+
50+
test('plays video', () => {
51+
const spy = jest.spyOn(video, 'play');
52+
53+
// ...
54+
});
55+
```

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

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
3030
"jest/no-jasmine-globals": "error",
3131
"jest/no-large-snapshots": "error",
3232
"jest/no-mocks-import": "error",
33+
"jest/no-restricted-jest-methods": "error",
3334
"jest/no-restricted-matchers": "error",
3435
"jest/no-standalone-expect": "error",
3536
"jest/no-test-prefixes": "error",

src/__tests__/rules.test.ts

+1-1
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 = 50;
5+
const numberOfRules = 51;
66
const ruleNames = Object.keys(plugin.rules);
77
const deprecatedRules = Object.entries(plugin.rules)
88
.filter(([, rule]) => rule.meta.deprecated)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { TSESLint } from '@typescript-eslint/utils';
2+
import dedent from 'dedent';
3+
import rule from '../no-restricted-jest-methods';
4+
import { espreeParser } from './test-utils';
5+
6+
const ruleTester = new TSESLint.RuleTester({
7+
parser: espreeParser,
8+
parserOptions: {
9+
ecmaVersion: 2017,
10+
},
11+
});
12+
13+
ruleTester.run('no-restricted-jest-methods', rule, {
14+
valid: [
15+
'jest',
16+
'jest.mock()',
17+
'expect(a).rejects;',
18+
'expect(a);',
19+
{
20+
code: dedent`
21+
import { jest } from '@jest/globals';
22+
23+
jest;
24+
`,
25+
parserOptions: { sourceType: 'module' },
26+
},
27+
],
28+
invalid: [
29+
{
30+
code: 'jest.fn()',
31+
options: [{ fn: null }],
32+
errors: [
33+
{
34+
messageId: 'restrictedJestMethod',
35+
data: {
36+
message: null,
37+
restriction: 'fn',
38+
},
39+
column: 6,
40+
line: 1,
41+
},
42+
],
43+
},
44+
{
45+
code: 'jest["fn"]()',
46+
options: [{ fn: null }],
47+
errors: [
48+
{
49+
messageId: 'restrictedJestMethod',
50+
data: {
51+
message: null,
52+
restriction: 'fn',
53+
},
54+
column: 6,
55+
line: 1,
56+
},
57+
],
58+
},
59+
{
60+
code: 'jest.mock()',
61+
options: [{ mock: 'Do not use mocks' }],
62+
errors: [
63+
{
64+
messageId: 'restrictedJestMethodWithMessage',
65+
data: {
66+
message: 'Do not use mocks',
67+
restriction: 'mock',
68+
},
69+
column: 6,
70+
line: 1,
71+
},
72+
],
73+
},
74+
{
75+
code: 'jest["mock"]()',
76+
options: [{ mock: 'Do not use mocks' }],
77+
errors: [
78+
{
79+
messageId: 'restrictedJestMethodWithMessage',
80+
data: {
81+
message: 'Do not use mocks',
82+
restriction: 'mock',
83+
},
84+
column: 6,
85+
line: 1,
86+
},
87+
],
88+
},
89+
{
90+
code: dedent`
91+
import { jest } from '@jest/globals';
92+
93+
jest.advanceTimersByTime();
94+
`,
95+
options: [{ advanceTimersByTime: null }],
96+
parserOptions: { sourceType: 'module' },
97+
errors: [
98+
{
99+
messageId: 'restrictedJestMethod',
100+
data: {
101+
message: null,
102+
restriction: 'advanceTimersByTime',
103+
},
104+
column: 6,
105+
line: 3,
106+
},
107+
],
108+
},
109+
],
110+
});
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { createRule, getAccessorValue, parseJestFnCall } from './utils';
2+
3+
const messages = {
4+
restrictedJestMethod: 'Use of `{{ restriction }}` is disallowed',
5+
restrictedJestMethodWithMessage: '{{ message }}',
6+
};
7+
8+
export default createRule<
9+
[Record<string, string | null>],
10+
keyof typeof messages
11+
>({
12+
name: __filename,
13+
meta: {
14+
docs: {
15+
category: 'Best Practices',
16+
description: 'Disallow specific `jest.` methods',
17+
recommended: false,
18+
},
19+
type: 'suggestion',
20+
schema: [
21+
{
22+
type: 'object',
23+
additionalProperties: {
24+
type: ['string', 'null'],
25+
},
26+
},
27+
],
28+
messages,
29+
},
30+
defaultOptions: [{}],
31+
create(context, [restrictedMethods]) {
32+
return {
33+
CallExpression(node) {
34+
const jestFnCall = parseJestFnCall(node, context);
35+
36+
if (jestFnCall?.type !== 'jest') {
37+
return;
38+
}
39+
40+
const method = getAccessorValue(jestFnCall.members[0]);
41+
42+
if (method in restrictedMethods) {
43+
const message = restrictedMethods[method];
44+
45+
context.report({
46+
messageId: message
47+
? 'restrictedJestMethodWithMessage'
48+
: 'restrictedJestMethod',
49+
data: { message, restriction: method },
50+
loc: {
51+
start: jestFnCall.members[0].loc.start,
52+
end: jestFnCall.members[jestFnCall.members.length - 1].loc.end,
53+
},
54+
});
55+
}
56+
},
57+
};
58+
},
59+
});

0 commit comments

Comments
 (0)