Skip to content

Commit 175f0a4

Browse files
committed
feat: create no-unhooked-function-calls rule
1 parent 7a49c58 commit 175f0a4

File tree

6 files changed

+272
-1
lines changed

6 files changed

+272
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ installations requiring long-term consistency.
177177
| [no-standalone-expect](docs/rules/no-standalone-expect.md) | Disallow using `expect` outside of `it` or `test` blocks | ![recommended][] | |
178178
| [no-test-prefixes](docs/rules/no-test-prefixes.md) | Use `.only` and `.skip` over `f` and `x` | ![recommended][] | ![fixable][] |
179179
| [no-test-return-statement](docs/rules/no-test-return-statement.md) | Disallow explicitly returning from tests | | |
180+
| [no-unhooked-function-calls](docs/rules/no-unhooked-function-calls.md) | Checks for function calls within describes that are not in a hook | | |
180181
| [prefer-called-with](docs/rules/prefer-called-with.md) | Suggest using `toBeCalledWith()` or `toHaveBeenCalledWith()` | | |
181182
| [prefer-expect-assertions](docs/rules/prefer-expect-assertions.md) | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | ![suggest][] |
182183
| [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | ![fixable][] |
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Checks for function calls within describes that are not in a hook (`no-unhooked-function-calls`)
2+
3+
Often while writing tests you have some setup work that needs to happen before
4+
tests run, and you have some finishing work that needs to happen after tests
5+
run. Jest provides helper functions to handle this.
6+
7+
It's common when writing tests to need to perform setup work that needs to
8+
happen before tests run, and finishing work after tests run.
9+
10+
Because Jest executes all `describe` handlers in a test file _before_ it
11+
executes any of the actual tests, it's important to ensure setup and teardown
12+
work is done inside `before*` and `after*` handlers respectively, rather than
13+
inside the `describe` blocks.
14+
15+
## Rule details
16+
17+
This rule flags any function calls within test files that are directly within
18+
the body of a `describe`, and suggests wrapping them in one of the four
19+
lifecycle hooks.
20+
21+
The following patterns are considered warnings:
22+
23+
```js
24+
describe('cities', () => {
25+
initializeCityDatabase();
26+
27+
test('city database has Vienna', () => {
28+
expect(isCity('Vienna')).toBeTruthy();
29+
});
30+
31+
test('city database has San Juan', () => {
32+
expect(isCity('San Juan')).toBeTruthy();
33+
});
34+
35+
clearCityDatabase();
36+
});
37+
```
38+
39+
The following patterns are **not** considered warnings:
40+
41+
```js
42+
describe('cities', () => {
43+
beforeEach(() => {
44+
initializeCityDatabase();
45+
});
46+
47+
test('city database has Vienna', () => {
48+
expect(isCity('Vienna')).toBeTruthy();
49+
});
50+
51+
test('city database has San Juan', () => {
52+
expect(isCity('San Juan')).toBeTruthy();
53+
});
54+
55+
afterEach(() => {
56+
clearCityDatabase();
57+
});
58+
});
59+
```

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

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Object {
3535
"jest/no-standalone-expect": "error",
3636
"jest/no-test-prefixes": "error",
3737
"jest/no-test-return-statement": "error",
38+
"jest/no-unhooked-function-calls": "error",
3839
"jest/prefer-called-with": "error",
3940
"jest/prefer-expect-assertions": "error",
4041
"jest/prefer-expect-resolves": "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 = 48;
5+
const numberOfRules = 49;
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,146 @@
1+
import { TSESLint } from '@typescript-eslint/experimental-utils';
2+
import dedent from 'dedent';
3+
import resolveFrom from 'resolve-from';
4+
import rule from '../no-unhooked-function-calls';
5+
6+
const ruleTester = new TSESLint.RuleTester({
7+
parser: resolveFrom(require.resolve('eslint'), 'espree'),
8+
parserOptions: {
9+
ecmaVersion: 2017,
10+
},
11+
});
12+
13+
ruleTester.run('no-unhooked-function-calls', rule, {
14+
valid: [
15+
dedent`
16+
test('it', () => {
17+
//
18+
});
19+
`,
20+
dedent`
21+
describe('some tests', () => {
22+
it('is true', () => {
23+
expect(true).toBe(true);
24+
});
25+
});
26+
`,
27+
dedent`
28+
describe('some tests', () => {
29+
it('is true', () => {
30+
expect(true).toBe(true);
31+
});
32+
33+
describe('more tests', () => {
34+
it('is false', () => {
35+
expect(true).toBe(false);
36+
});
37+
});
38+
});
39+
`,
40+
dedent`
41+
describe('some tests', () => {
42+
let consoleLogSpy;
43+
44+
beforeEach(() => {
45+
consoleLogSpy = jest.spyOn(console, 'log');
46+
});
47+
48+
it('prints a message', () => {
49+
printMessage('hello world');
50+
51+
expect(consoleLogSpy).toHaveBeenCalledWith('hello world');
52+
});
53+
});
54+
`,
55+
dedent`
56+
describe('some tests', () => {
57+
beforeEach(() => {
58+
setup();
59+
});
60+
});
61+
`,
62+
dedent`
63+
beforeEach(() => {
64+
initializeCityDatabase();
65+
});
66+
67+
afterEach(() => {
68+
clearCityDatabase();
69+
});
70+
71+
test('city database has Vienna', () => {
72+
expect(isCity('Vienna')).toBeTruthy();
73+
});
74+
75+
test('city database has San Juan', () => {
76+
expect(isCity('San Juan')).toBeTruthy();
77+
});
78+
`,
79+
dedent`
80+
describe('cities', () => {
81+
beforeEach(() => {
82+
initializeCityDatabase();
83+
});
84+
85+
test('city database has Vienna', () => {
86+
expect(isCity('Vienna')).toBeTruthy();
87+
});
88+
89+
test('city database has San Juan', () => {
90+
expect(isCity('San Juan')).toBeTruthy();
91+
});
92+
93+
afterEach(() => {
94+
clearCityDatabase();
95+
});
96+
});
97+
`,
98+
],
99+
invalid: [
100+
{
101+
code: dedent`
102+
describe('some tests', () => {
103+
setup();
104+
});
105+
`,
106+
errors: [
107+
{
108+
messageId: 'useHook',
109+
line: 2,
110+
column: 3,
111+
},
112+
],
113+
},
114+
{
115+
code: dedent`
116+
describe('some tests', () => {
117+
setup();
118+
119+
it('is true', () => {
120+
expect(true).toBe(true);
121+
});
122+
123+
describe('more tests', () => {
124+
setup();
125+
126+
it('is false', () => {
127+
expect(true).toBe(false);
128+
});
129+
});
130+
});
131+
`,
132+
errors: [
133+
{
134+
messageId: 'useHook',
135+
line: 2,
136+
column: 3,
137+
},
138+
{
139+
messageId: 'useHook',
140+
line: 9,
141+
column: 5,
142+
},
143+
],
144+
},
145+
],
146+
});
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
2+
import {
3+
createRule,
4+
isDescribeCall,
5+
isFunction,
6+
isHook,
7+
isTestCaseCall,
8+
} from './utils';
9+
10+
export default createRule({
11+
name: __filename,
12+
meta: {
13+
docs: {
14+
category: 'Best Practices',
15+
description:
16+
'Checks for function calls within describes that are not in a hook',
17+
recommended: false,
18+
},
19+
messages: {
20+
useHook: 'This should be done within a hook',
21+
},
22+
type: 'suggestion',
23+
schema: [],
24+
},
25+
defaultOptions: [],
26+
create(context) {
27+
return {
28+
CallExpression(node) {
29+
if (!isDescribeCall(node) || node.arguments.length < 2) {
30+
return;
31+
}
32+
33+
const [, testFn] = node.arguments;
34+
35+
if (
36+
!isFunction(testFn) ||
37+
testFn.body.type !== AST_NODE_TYPES.BlockStatement
38+
) {
39+
return;
40+
}
41+
42+
for (const nod of testFn.body.body) {
43+
if (
44+
nod.type === AST_NODE_TYPES.ExpressionStatement &&
45+
nod.expression.type === AST_NODE_TYPES.CallExpression
46+
) {
47+
if (
48+
isDescribeCall(nod.expression) ||
49+
isTestCaseCall(nod.expression) ||
50+
isHook(nod.expression)
51+
) {
52+
return;
53+
}
54+
55+
context.report({
56+
node: nod.expression,
57+
messageId: 'useHook',
58+
});
59+
}
60+
}
61+
},
62+
};
63+
},
64+
});

0 commit comments

Comments
 (0)