Skip to content

Commit 3e6cb44

Browse files
G-RathSimenB
authored andcommitted
feat(no-done-callback): support hooks (#656)
BREAKING CHANGE: `no-done-callback` will now report hooks using callbacks as well, not just tests Fixes #649 Resolves #651
1 parent 99774cd commit 3e6cb44

File tree

4 files changed

+268
-51
lines changed

4 files changed

+268
-51
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ installations requiring long-term consistency.
138138
| [no-conditional-expect](docs/rules/no-conditional-expect.md) | Prevent calling `expect` conditionally | ![recommended][] | |
139139
| [no-deprecated-functions](docs/rules/no-deprecated-functions.md) | Disallow use of deprecated functions | ![recommended][] | ![fixable][] |
140140
| [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | ![recommended][] | |
141-
| [no-done-callback](docs/rules/no-done-callback.md) | Avoid using a callback in asynchronous tests | ![recommended][] | ![suggest][] |
141+
| [no-done-callback](docs/rules/no-done-callback.md) | Avoid using a callback in asynchronous tests and hooks | ![recommended][] | ![suggest][] |
142142
| [no-duplicate-hooks](docs/rules/no-duplicate-hooks.md) | Disallow duplicate setup and teardown hooks | | |
143143
| [no-export](docs/rules/no-export.md) | Disallow using `exports` in files containing tests | ![recommended][] | |
144144
| [no-focused-tests](docs/rules/no-focused-tests.md) | Disallow focused tests | ![recommended][] | ![fixable][] |

docs/rules/no-done-callback.md

+43-29
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,66 @@
1-
# Avoid using a callback in asynchronous tests (`no-done-callback`)
1+
# Avoid using a callback in asynchronous tests and hooks (`no-done-callback`)
22

3-
Jest allows you to pass a callback to test definitions, typically called `done`,
4-
that is later invoked to indicate that the asynchronous test is complete.
3+
When calling asynchronous code in hooks and tests, `jest` needs to know when the
4+
asynchronous work is complete to progress the current run.
55

6-
However, that means that if your test throws (e.g. because of a failing
7-
assertion), `done` will never be called unless you manually use `try-catch`.
6+
Originally the most common pattern to archive this was to use callbacks:
87

98
```js
10-
test('some test', done => {
11-
expect(false).toBe(true);
12-
done();
9+
test('the data is peanut butter', done => {
10+
function callback(data) {
11+
try {
12+
expect(data).toBe('peanut butter');
13+
done();
14+
} catch (error) {
15+
done(error);
16+
}
17+
}
18+
19+
fetchData(callback);
1320
});
1421
```
1522

16-
The test above will time out instead of failing the assertions, since `done` is
17-
never called.
23+
This can be very error prone however, as it requires careful understanding of
24+
how assertions work in tests or otherwise tests won't behave as expected.
1825

19-
Correct way of doing the same thing is to wrap it in `try-catch`.
26+
For example, if the `try/catch` was left out of the above code, the test would
27+
timeout rather than fail. Even with the `try/catch`, forgetting to pass the
28+
caught error to `done` will result in `jest` believing the test has passed.
29+
30+
A more straightforward way to handle asynchronous code is to use Promises:
2031

2132
```js
22-
test('some test', done => {
23-
try {
24-
expect(false).toBe(true);
25-
done();
26-
} catch (e) {
27-
done(e);
28-
}
33+
test('the data is peanut butter', () => {
34+
return fetchData().then(data => {
35+
expect(data).toBe('peanut butter');
36+
});
2937
});
3038
```
3139

32-
However, Jest supports a second way of having asynchronous tests - using
33-
promises.
40+
When a test or hook returns a promise, `jest` waits for that promise to resolve,
41+
as well as automatically failing should the promise reject.
42+
43+
If your environment supports `async/await`, this becomes even simpler:
3444

3545
```js
36-
test('some test', () => {
37-
return new Promise(done => {
38-
expect(false).toBe(true);
39-
done();
40-
});
46+
test('the data is peanut butter', async () => {
47+
const data = await fetchData();
48+
expect(data).toBe('peanut butter');
4149
});
4250
```
4351

44-
Even though `done` is never called here, the Promise will still reject, and Jest
45-
will report the assertion error correctly.
46-
4752
## Rule details
4853

49-
This rule triggers a warning if you have a `done` callback in your test.
54+
This rule checks the function parameter of hooks & tests for use of the `done`
55+
argument, suggesting you return a promise instead.
5056

5157
The following patterns are considered warnings:
5258

5359
```js
60+
beforeEach(done => {
61+
// ...
62+
});
63+
5464
test('myFunction()', done => {
5565
// ...
5666
});
@@ -63,6 +73,10 @@ test('myFunction()', function (done) {
6373
The following patterns are not considered warnings:
6474

6575
```js
76+
beforeEach(async () => {
77+
await setupUsTheBomb();
78+
});
79+
6680
test('myFunction()', () => {
6781
expect(myFunction()).toBeTruthy();
6882
});

src/rules/__tests__/no-done-callback.test.ts

+194-9
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ ruleTester.run('no-done-callback', rule, {
1717
'test("something", function() {})',
1818
'test("something", async function () {})',
1919
'test("something", someArg)',
20+
'beforeEach(() => {})',
21+
'beforeAll(async () => {})',
22+
'afterAll(() => {})',
23+
'afterAll(async function () {})',
24+
'afterAll(async function () {}, 5)',
2025
],
2126
invalid: [
2227
{
2328
code: 'test("something", (...args) => {args[0]();})',
2429
errors: [
2530
{
26-
messageId: 'illegalTestCallback',
31+
messageId: 'noDoneCallback',
2732
line: 1,
2833
column: 20,
2934
},
@@ -33,7 +38,7 @@ ruleTester.run('no-done-callback', rule, {
3338
code: 'test("something", done => {done();})',
3439
errors: [
3540
{
36-
messageId: 'illegalTestCallback',
41+
messageId: 'noDoneCallback',
3742
line: 1,
3843
column: 19,
3944
suggestions: [
@@ -51,7 +56,7 @@ ruleTester.run('no-done-callback', rule, {
5156
code: 'test("something", finished => {finished();})',
5257
errors: [
5358
{
54-
messageId: 'illegalTestCallback',
59+
messageId: 'noDoneCallback',
5560
line: 1,
5661
column: 19,
5762
suggestions: [
@@ -69,7 +74,7 @@ ruleTester.run('no-done-callback', rule, {
6974
code: 'test("something", (done) => {done();})',
7075
errors: [
7176
{
72-
messageId: 'illegalTestCallback',
77+
messageId: 'noDoneCallback',
7378
line: 1,
7479
column: 20,
7580
suggestions: [
@@ -87,7 +92,7 @@ ruleTester.run('no-done-callback', rule, {
8792
code: 'test("something", done => done())',
8893
errors: [
8994
{
90-
messageId: 'illegalTestCallback',
95+
messageId: 'noDoneCallback',
9196
line: 1,
9297
column: 19,
9398
suggestions: [
@@ -104,7 +109,7 @@ ruleTester.run('no-done-callback', rule, {
104109
code: 'test("something", (done) => done())',
105110
errors: [
106111
{
107-
messageId: 'illegalTestCallback',
112+
messageId: 'noDoneCallback',
108113
line: 1,
109114
column: 20,
110115
suggestions: [
@@ -121,7 +126,7 @@ ruleTester.run('no-done-callback', rule, {
121126
code: 'test("something", function(done) {done();})',
122127
errors: [
123128
{
124-
messageId: 'illegalTestCallback',
129+
messageId: 'noDoneCallback',
125130
line: 1,
126131
column: 28,
127132
suggestions: [
@@ -139,7 +144,7 @@ ruleTester.run('no-done-callback', rule, {
139144
code: 'test("something", function (done) {done();})',
140145
errors: [
141146
{
142-
messageId: 'illegalTestCallback',
147+
messageId: 'noDoneCallback',
143148
line: 1,
144149
column: 29,
145150
suggestions: [
@@ -183,7 +188,7 @@ ruleTester.run('no-done-callback', rule, {
183188
`,
184189
errors: [
185190
{
186-
messageId: 'illegalTestCallback',
191+
messageId: 'noDoneCallback',
187192
line: 1,
188193
column: 20,
189194
suggestions: [
@@ -200,5 +205,185 @@ ruleTester.run('no-done-callback', rule, {
200205
},
201206
],
202207
},
208+
{
209+
code: 'afterEach((...args) => {args[0]();})',
210+
errors: [
211+
{
212+
messageId: 'noDoneCallback',
213+
line: 1,
214+
column: 12,
215+
},
216+
],
217+
},
218+
{
219+
code: 'beforeAll(done => {done();})',
220+
errors: [
221+
{
222+
messageId: 'noDoneCallback',
223+
line: 1,
224+
column: 11,
225+
suggestions: [
226+
{
227+
messageId: 'suggestWrappingInPromise',
228+
data: { callback: 'done' },
229+
output:
230+
'beforeAll(() => {return new Promise(done => {done();})})',
231+
},
232+
],
233+
},
234+
],
235+
},
236+
{
237+
code: 'beforeAll(finished => {finished();})',
238+
errors: [
239+
{
240+
messageId: 'noDoneCallback',
241+
line: 1,
242+
column: 11,
243+
suggestions: [
244+
{
245+
messageId: 'suggestWrappingInPromise',
246+
data: { callback: 'finished' },
247+
output:
248+
'beforeAll(() => {return new Promise(finished => {finished();})})',
249+
},
250+
],
251+
},
252+
],
253+
},
254+
{
255+
code: 'beforeEach((done) => {done();})',
256+
errors: [
257+
{
258+
messageId: 'noDoneCallback',
259+
line: 1,
260+
column: 13,
261+
suggestions: [
262+
{
263+
messageId: 'suggestWrappingInPromise',
264+
data: { callback: 'done' },
265+
output:
266+
'beforeEach(() => {return new Promise((done) => {done();})})',
267+
},
268+
],
269+
},
270+
],
271+
},
272+
{
273+
code: 'afterAll(done => done())',
274+
errors: [
275+
{
276+
messageId: 'noDoneCallback',
277+
line: 1,
278+
column: 10,
279+
suggestions: [
280+
{
281+
messageId: 'suggestWrappingInPromise',
282+
data: { callback: 'done' },
283+
output: 'afterAll(() => new Promise(done => done()))',
284+
},
285+
],
286+
},
287+
],
288+
},
289+
{
290+
code: 'afterEach((done) => done())',
291+
errors: [
292+
{
293+
messageId: 'noDoneCallback',
294+
line: 1,
295+
column: 12,
296+
suggestions: [
297+
{
298+
messageId: 'suggestWrappingInPromise',
299+
data: { callback: 'done' },
300+
output: 'afterEach(() => new Promise((done) => done()))',
301+
},
302+
],
303+
},
304+
],
305+
},
306+
{
307+
code: 'beforeAll(function(done) {done();})',
308+
errors: [
309+
{
310+
messageId: 'noDoneCallback',
311+
line: 1,
312+
column: 20,
313+
suggestions: [
314+
{
315+
messageId: 'suggestWrappingInPromise',
316+
data: { callback: 'done' },
317+
output:
318+
'beforeAll(function() {return new Promise((done) => {done();})})',
319+
},
320+
],
321+
},
322+
],
323+
},
324+
{
325+
code: 'afterEach(function (done) {done();})',
326+
errors: [
327+
{
328+
messageId: 'noDoneCallback',
329+
line: 1,
330+
column: 21,
331+
suggestions: [
332+
{
333+
messageId: 'suggestWrappingInPromise',
334+
data: { callback: 'done' },
335+
output:
336+
'afterEach(function () {return new Promise((done) => {done();})})',
337+
},
338+
],
339+
},
340+
],
341+
},
342+
{
343+
code: 'beforeAll(async done => {done();})',
344+
errors: [{ messageId: 'useAwaitInsteadOfCallback', line: 1, column: 17 }],
345+
},
346+
{
347+
code: 'beforeAll(async done => done())',
348+
errors: [{ messageId: 'useAwaitInsteadOfCallback', line: 1, column: 17 }],
349+
},
350+
{
351+
code: 'beforeAll(async function (done) {done();})',
352+
errors: [{ messageId: 'useAwaitInsteadOfCallback', line: 1, column: 27 }],
353+
},
354+
{
355+
code: dedent`
356+
afterAll(async (done) => {
357+
await myAsyncTask();
358+
done();
359+
});
360+
`,
361+
errors: [{ messageId: 'useAwaitInsteadOfCallback', line: 1, column: 17 }],
362+
},
363+
{
364+
code: dedent`
365+
beforeEach((done) => {
366+
done();
367+
});
368+
`,
369+
errors: [
370+
{
371+
messageId: 'noDoneCallback',
372+
line: 1,
373+
column: 13,
374+
suggestions: [
375+
{
376+
messageId: 'suggestWrappingInPromise',
377+
data: { callback: 'done' },
378+
output: dedent`
379+
beforeEach(() => {return new Promise((done) => {
380+
done();
381+
})});
382+
`,
383+
},
384+
],
385+
},
386+
],
387+
},
203388
],
204389
});

0 commit comments

Comments
 (0)