Skip to content

Commit 6204b31

Browse files
authored
feat: create require-hook rule (#929)
1 parent cc12c7c commit 6204b31

File tree

6 files changed

+487
-1
lines changed

6 files changed

+487
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ installations requiring long-term consistency.
189189
| [prefer-to-contain](docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | ![style][] | ![fixable][] |
190190
| [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | ![style][] | ![fixable][] |
191191
| [prefer-todo](docs/rules/prefer-todo.md) | Suggest using `test.todo` | | ![fixable][] |
192+
| [require-hook](docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | |
192193
| [require-to-throw-message](docs/rules/require-to-throw-message.md) | Require a message for `toThrow()` | | |
193194
| [require-top-level-describe](docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `describe` block | | |
194195
| [valid-describe](docs/rules/valid-describe.md) | Enforce valid `describe()` callback | ![recommended][] | |

docs/rules/require-hook.md

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Require setup and teardown code to be within a hook (`require-hook`)
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 expression that is either at the toplevel of a test file or
18+
directly within the body of a `describe`, _except_ for the following:
19+
20+
- `import` statements
21+
- `const` variables
22+
- `let` _declarations_
23+
- Types
24+
- Calls to the standard Jest globals
25+
26+
This rule flags any function calls within test files that are directly within
27+
the body of a `describe`, and suggests wrapping them in one of the four
28+
lifecycle hooks.
29+
30+
Here is a slightly contrived test file showcasing some common cases that would
31+
be flagged:
32+
33+
```js
34+
import { database, isCity } from '../database';
35+
import { Logger } from '../../../src/Logger';
36+
import { loadCities } from '../api';
37+
38+
jest.mock('../api');
39+
40+
const initializeCityDatabase = () => {
41+
database.addCity('Vienna');
42+
database.addCity('San Juan');
43+
database.addCity('Wellington');
44+
};
45+
46+
const clearCityDatabase = () => {
47+
database.clear();
48+
};
49+
50+
initializeCityDatabase();
51+
52+
test('that persists cities', () => {
53+
expect(database.cities.length).toHaveLength(3);
54+
});
55+
56+
test('city database has Vienna', () => {
57+
expect(isCity('Vienna')).toBeTruthy();
58+
});
59+
60+
test('city database has San Juan', () => {
61+
expect(isCity('San Juan')).toBeTruthy();
62+
});
63+
64+
describe('when loading cities from the api', () => {
65+
let consoleWarnSpy = jest.spyOn(console, 'warn');
66+
67+
loadCities.mockResolvedValue(['Wellington', 'London']);
68+
69+
it('does not duplicate cities', async () => {
70+
await database.loadCities();
71+
72+
expect(database.cities).toHaveLength(4);
73+
});
74+
75+
it('logs any duplicates', async () => {
76+
await database.loadCities();
77+
78+
expect(consoleWarnSpy).toHaveBeenCalledWith(
79+
'Ignored duplicate cities: Wellington',
80+
);
81+
});
82+
});
83+
84+
clearCityDatabase();
85+
```
86+
87+
Here is the same slightly contrived test file showcasing the same common cases
88+
but in ways that would be **not** flagged:
89+
90+
```js
91+
import { database, isCity } from '../database';
92+
import { Logger } from '../../../src/Logger';
93+
import { loadCities } from '../api';
94+
95+
jest.mock('../api');
96+
97+
const initializeCityDatabase = () => {
98+
database.addCity('Vienna');
99+
database.addCity('San Juan');
100+
database.addCity('Wellington');
101+
};
102+
103+
const clearCityDatabase = () => {
104+
database.clear();
105+
};
106+
107+
beforeEach(() => {
108+
initializeCityDatabase();
109+
});
110+
111+
test('that persists cities', () => {
112+
expect(database.cities.length).toHaveLength(3);
113+
});
114+
115+
test('city database has Vienna', () => {
116+
expect(isCity('Vienna')).toBeTruthy();
117+
});
118+
119+
test('city database has San Juan', () => {
120+
expect(isCity('San Juan')).toBeTruthy();
121+
});
122+
123+
describe('when loading cities from the api', () => {
124+
let consoleWarnSpy;
125+
126+
beforeEach(() => {
127+
consoleWarnSpy = jest.spyOn(console, 'warn');
128+
loadCities.mockResolvedValue(['Wellington', 'London']);
129+
});
130+
131+
it('does not duplicate cities', async () => {
132+
await database.loadCities();
133+
134+
expect(database.cities).toHaveLength(4);
135+
});
136+
137+
it('logs any duplicates', async () => {
138+
await database.loadCities();
139+
140+
expect(consoleWarnSpy).toHaveBeenCalledWith(
141+
'Ignored duplicate cities: Wellington',
142+
);
143+
});
144+
});
145+
146+
afterEach(() => {
147+
clearCityDatabase();
148+
});
149+
```

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

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Object {
4747
"jest/prefer-to-contain": "error",
4848
"jest/prefer-to-have-length": "error",
4949
"jest/prefer-todo": "error",
50+
"jest/require-hook": "error",
5051
"jest/require-to-throw-message": "error",
5152
"jest/require-top-level-describe": "error",
5253
"jest/unbound-method": "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)

0 commit comments

Comments
 (0)