Skip to content

Commit 178744b

Browse files
macklinuSimenB
authored andcommitted
feat(rule): add no-hooks rule (#74)
Resolves #72
1 parent 38f8ca0 commit 178744b

File tree

5 files changed

+274
-0
lines changed

5 files changed

+274
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ for more information about extending configuration files.
8383
| [consistent-test-it](docs/rules/consistent-test-it.md) | Enforce consistent test or it keyword | | ![fixable](https://img.shields.io/badge/-fixable-green.svg) |
8484
| [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | |
8585
| [no-focused-tests](docs/rules/no-focused-tests.md) | Disallow focused tests | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | |
86+
| [no-hooks](docs/rules/no-hooks.md) | Disallow setup and teardown hooks | | |
8687
| [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | |
8788
| [no-large-snapshots](docs/rules/no-large-snapshots.md) | Disallow large snapshots | | |
8889
| [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | ![fixable](https://img.shields.io/badge/-fixable-green.svg) |

docs/rules/no-hooks.md

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Disallow setup and teardown hooks (no-hooks)
2+
3+
Jest provides global functions for setup and teardown tasks, which are called
4+
before/after each test case and each test suite. The use of these hooks promotes
5+
shared state between tests.
6+
7+
## Rule Details
8+
9+
This rule reports for the following function calls:
10+
11+
* `beforeAll`
12+
* `beforeEach`
13+
* `afterAll`
14+
* `afterEach`
15+
16+
Examples of **incorrect** code for this rule:
17+
18+
```js
19+
/* eslint jest/no-hooks: "error" */
20+
21+
function setupFoo(options) {
22+
/* ... */
23+
}
24+
25+
function setupBar(options) {
26+
/* ... */
27+
}
28+
29+
describe('foo', () => {
30+
let foo;
31+
32+
beforeEach(() => {
33+
foo = setupFoo();
34+
});
35+
36+
afterEach(() => {
37+
foo = null;
38+
});
39+
40+
it('does something', () => {
41+
expect(foo.doesSomething()).toBe(true);
42+
});
43+
44+
describe('with bar', () => {
45+
let bar;
46+
47+
beforeEach(() => {
48+
bar = setupBar();
49+
});
50+
51+
afterEach(() => {
52+
bar = null;
53+
});
54+
55+
it('does something with bar', () => {
56+
expect(foo.doesSomething(bar)).toBe(true);
57+
});
58+
});
59+
});
60+
```
61+
62+
Examples of **correct** code for this rule:
63+
64+
```js
65+
/* eslint jest/no-hooks: "error" */
66+
67+
function setupFoo(options) {
68+
/* ... */
69+
}
70+
71+
function setupBar(options) {
72+
/* ... */
73+
}
74+
75+
describe('foo', () => {
76+
it('does something', () => {
77+
const foo = setupFoo();
78+
expect(foo.doesSomething()).toBe(true);
79+
});
80+
81+
it('does something with bar', () => {
82+
const foo = setupFoo();
83+
const bar = setupBar();
84+
expect(foo.doesSomething(bar)).toBe(true);
85+
});
86+
});
87+
```
88+
89+
## Options
90+
91+
```json
92+
{
93+
"jest/no-hooks": [
94+
"error",
95+
{
96+
"allow": ["afterEach", "afterAll"]
97+
}
98+
]
99+
}
100+
```
101+
102+
### `allow`
103+
104+
This array option whitelists setup and teardown hooks so that this rule does not
105+
report their usage as being incorrect. There are four possible values:
106+
107+
* `"beforeAll"`
108+
* `"beforeEach"`
109+
* `"afterAll"`
110+
* `"afterEach"`
111+
112+
By default, none of these options are enabled (the equivalent of
113+
`{ "allow": [] }`).
114+
115+
Examples of **incorrect** code for the `{ "allow": ["afterEach"] }` option:
116+
117+
```js
118+
/* eslint jest/no-hooks: ["error", { "allow": ["afterEach"] }] */
119+
120+
function setupFoo(options) {
121+
/* ... */
122+
}
123+
124+
let foo;
125+
126+
beforeEach(() => {
127+
foo = setupFoo();
128+
});
129+
130+
afterEach(() => {
131+
jest.resetModules();
132+
});
133+
134+
test('foo does this', () => {
135+
// ...
136+
});
137+
138+
test('foo does that', () => {
139+
// ...
140+
});
141+
```
142+
143+
Examples of **correct** code for the `{ "allow": ["afterEach"] }` option:
144+
145+
```js
146+
/* eslint jest/no-hooks: ["error", { "allow": ["afterEach"] }] */
147+
148+
function setupFoo(options) {
149+
/* ... */
150+
}
151+
152+
afterEach(() => {
153+
jest.resetModules();
154+
});
155+
156+
test('foo does this', () => {
157+
const foo = setupFoo();
158+
// ...
159+
});
160+
161+
test('foo does that', () => {
162+
const foo = setupFoo();
163+
// ...
164+
});
165+
```
166+
167+
## When Not To Use It
168+
169+
If you prefer using the setup and teardown hooks provided by Jest, you can
170+
safely disable this rule.
171+
172+
## Further Reading
173+
174+
* [Jest docs - Setup and Teardown](https://facebook.github.io/jest/docs/en/setup-teardown.html)
175+
* [@jamiebuilds Twitter thread](https://twitter.com/jamiebuilds/status/954906997169664000)

index.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const consistentTestIt = require('./rules/consistent-test-it');
44
const noDisabledTests = require('./rules/no-disabled-tests');
55
const noFocusedTests = require('./rules/no-focused-tests');
6+
const noHooks = require('./rules/no-hooks');
67
const noIdenticalTitle = require('./rules/no-identical-title');
78
const noLargeSnapshots = require('./rules/no-large-snapshots');
89
const preferToBeNull = require('./rules/prefer-to-be-null');
@@ -61,6 +62,7 @@ module.exports = {
6162
'consistent-test-it': consistentTestIt,
6263
'no-disabled-tests': noDisabledTests,
6364
'no-focused-tests': noFocusedTests,
65+
'no-hooks': noHooks,
6466
'no-identical-title': noIdenticalTitle,
6567
'no-large-snapshots': noLargeSnapshots,
6668
'prefer-to-be-null': preferToBeNull,

rules/__tests__/no-hooks.test.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict';
2+
3+
const RuleTester = require('eslint').RuleTester;
4+
const rules = require('../..').rules;
5+
const ruleTester = new RuleTester({
6+
parserOptions: {
7+
ecmaVersion: 6,
8+
},
9+
});
10+
11+
ruleTester.run('no-hooks', rules['no-hooks'], {
12+
valid: [
13+
'test("foo")',
14+
'describe("foo", () => { it("bar") })',
15+
'test("foo", () => { expect(subject.beforeEach()).toBe(true) })',
16+
{
17+
code: 'afterEach(() => {}); afterAll(() => {});',
18+
options: [{ allow: ['afterEach', 'afterAll'] }],
19+
},
20+
],
21+
invalid: [
22+
{
23+
code: 'beforeAll(() => {})',
24+
errors: [{ message: "Unexpected 'beforeAll' hook" }],
25+
},
26+
{
27+
code: 'beforeEach(() => {})',
28+
errors: [{ message: "Unexpected 'beforeEach' hook" }],
29+
},
30+
{
31+
code: 'afterAll(() => {})',
32+
errors: [{ message: "Unexpected 'afterAll' hook" }],
33+
},
34+
{
35+
code: 'afterEach(() => {})',
36+
errors: [{ message: "Unexpected 'afterEach' hook" }],
37+
},
38+
{
39+
code: 'beforeEach(() => {}); afterEach(() => { jest.resetModules() });',
40+
options: [{ allow: ['afterEach'] }],
41+
errors: [{ message: "Unexpected 'beforeEach' hook" }],
42+
},
43+
],
44+
});

rules/no-hooks.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
3+
module.exports = {
4+
meta: {
5+
docs: {
6+
url:
7+
'https://github.com/jest-community/eslint-plugin-jest/blob/master/docs/rules/no-hooks.md',
8+
},
9+
},
10+
schema: [
11+
{
12+
type: 'object',
13+
properties: {
14+
allow: {
15+
type: 'array',
16+
contains: ['beforeAll', 'beforeEach', 'afterAll', 'afterEach'],
17+
},
18+
},
19+
additionalProperties: false,
20+
},
21+
],
22+
create(context) {
23+
const testHookNames = Object.assign(Object.create(null), {
24+
beforeAll: true,
25+
beforeEach: true,
26+
afterAll: true,
27+
afterEach: true,
28+
});
29+
30+
const whitelistedHookNames = (
31+
context.options[0] || { allow: [] }
32+
).allow.reduce((hashMap, value) => {
33+
hashMap[value] = true;
34+
return hashMap;
35+
}, Object.create(null));
36+
37+
const isHook = node => testHookNames[node.callee.name];
38+
const isWhitelisted = node => whitelistedHookNames[node.callee.name];
39+
40+
return {
41+
CallExpression(node) {
42+
if (isHook(node) && !isWhitelisted(node)) {
43+
context.report({
44+
node,
45+
message: "Unexpected '{{ hookName }}' hook",
46+
data: { hookName: node.callee.name },
47+
});
48+
}
49+
},
50+
};
51+
},
52+
};

0 commit comments

Comments
 (0)