-
Notifications
You must be signed in to change notification settings - Fork 237
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rules): add
no-test-callback
rule (#179)
- Loading branch information
Showing
5 changed files
with
287 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# Avoid using a callback in asynchronous tests (no-test-callback) | ||
|
||
Jest allows you to pass a callback to test definitions, typically called `done`, | ||
that is later invoked to indicate that the asynchronous test is complete. | ||
|
||
However, that means that if your test throws (e.g. because of a failing | ||
assertion), `done` will never be called unless you manually use `try-catch`. | ||
|
||
```js | ||
test('some test', done => { | ||
expect(false).toBe(true); | ||
done(); | ||
}); | ||
``` | ||
|
||
The test above will time out instead of failing the assertions, since `done` is | ||
never called. | ||
|
||
Correct way of doing the same thing is to wrap it in `try-catch`. | ||
|
||
```js | ||
test('some test', done => { | ||
try { | ||
expect(false).toBe(true); | ||
done(); | ||
} catch (e) { | ||
done(e); | ||
} | ||
}); | ||
``` | ||
|
||
However, Jest supports a second way of having asynchronous tests - using | ||
promises. | ||
|
||
```js | ||
test('some test', () => { | ||
return new Promise(done => { | ||
expect(false).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
``` | ||
|
||
Even though `done` is never called here, the Promise will still reject, and Jest | ||
will report the assertion error correctly. | ||
|
||
## Rule details | ||
|
||
This rule triggers a warning if you have a `done` callback in your test. | ||
|
||
The following patterns are considered warnings: | ||
|
||
```js | ||
test('myFunction()', done => { | ||
// ... | ||
}); | ||
|
||
test('myFunction()', function(done) { | ||
// ... | ||
}); | ||
``` | ||
|
||
The following patterns are not considered warnings: | ||
|
||
```js | ||
test('myFunction()', () => { | ||
expect(myFunction()).toBeTruthy(); | ||
}); | ||
|
||
test('myFunction()', () => { | ||
return new Promise(done => { | ||
expect(myFunction()).toBeTruthy(); | ||
done(); | ||
}); | ||
}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
'use strict'; | ||
|
||
const RuleTester = require('eslint').RuleTester; | ||
const rule = require('../no-test-callback'); | ||
|
||
const ruleTester = new RuleTester({ | ||
parserOptions: { | ||
ecmaVersion: 8, | ||
}, | ||
}); | ||
|
||
ruleTester.run('no-test-callback', rule, { | ||
valid: [ | ||
'test("something", () => {})', | ||
'test("something", async () => {})', | ||
'test("something", function() {})', | ||
'test("something", async function () {})', | ||
'test("something", someArg)', | ||
], | ||
invalid: [ | ||
{ | ||
code: 'test("something", done => {done();})', | ||
errors: [ | ||
{ | ||
message: 'Illegal usage of test callback', | ||
line: 1, | ||
column: 19, | ||
}, | ||
], | ||
output: | ||
'test("something", () => {return new Promise(done => {done();})})', | ||
}, | ||
{ | ||
code: 'test("something", (done) => {done();})', | ||
errors: [ | ||
{ | ||
message: 'Illegal usage of test callback', | ||
line: 1, | ||
column: 20, | ||
}, | ||
], | ||
output: | ||
'test("something", () => {return new Promise((done) => {done();})})', | ||
}, | ||
{ | ||
code: 'test("something", done => done())', | ||
errors: [ | ||
{ | ||
message: 'Illegal usage of test callback', | ||
line: 1, | ||
column: 19, | ||
}, | ||
], | ||
output: 'test("something", () => new Promise(done => done()))', | ||
}, | ||
{ | ||
code: 'test("something", (done) => done())', | ||
errors: [ | ||
{ | ||
message: 'Illegal usage of test callback', | ||
line: 1, | ||
column: 20, | ||
}, | ||
], | ||
output: 'test("something", () => new Promise((done) => done()))', | ||
}, | ||
{ | ||
code: 'test("something", function(done) {done();})', | ||
errors: [ | ||
{ | ||
message: 'Illegal usage of test callback', | ||
line: 1, | ||
column: 28, | ||
}, | ||
], | ||
output: | ||
'test("something", function() {return new Promise((done) => {done();})})', | ||
}, | ||
{ | ||
code: 'test("something", function (done) {done();})', | ||
errors: [ | ||
{ | ||
message: 'Illegal usage of test callback', | ||
line: 1, | ||
column: 29, | ||
}, | ||
], | ||
output: | ||
'test("something", function () {return new Promise((done) => {done();})})', | ||
}, | ||
{ | ||
code: 'test("something", async done => {done();})', | ||
errors: [ | ||
{ | ||
message: 'Illegal usage of test callback', | ||
line: 1, | ||
column: 25, | ||
}, | ||
], | ||
output: | ||
'test("something", async () => {await new Promise(done => {done();})})', | ||
}, | ||
{ | ||
code: 'test("something", async done => done())', | ||
errors: [ | ||
{ | ||
message: 'Illegal usage of test callback', | ||
line: 1, | ||
column: 25, | ||
}, | ||
], | ||
output: 'test("something", async () => new Promise(done => done()))', | ||
}, | ||
{ | ||
code: 'test("something", async function (done) {done();})', | ||
errors: [ | ||
{ | ||
message: 'Illegal usage of test callback', | ||
line: 1, | ||
column: 35, | ||
}, | ||
], | ||
output: | ||
'test("something", async function () {await new Promise((done) => {done();})})', | ||
}, | ||
], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
'use strict'; | ||
|
||
const getDocsUrl = require('./util').getDocsUrl; | ||
const isTestCase = require('./util').isTestCase; | ||
|
||
module.exports = { | ||
meta: { | ||
docs: { | ||
url: getDocsUrl(__filename), | ||
}, | ||
fixable: 'code', | ||
}, | ||
create(context) { | ||
return { | ||
CallExpression(node) { | ||
if (!isTestCase(node) || node.arguments.length !== 2) { | ||
return; | ||
} | ||
|
||
const callback = node.arguments[1]; | ||
|
||
if ( | ||
!/^(Arrow)?FunctionExpression$/.test(callback.type) || | ||
callback.params.length !== 1 | ||
) { | ||
return; | ||
} | ||
|
||
const argument = callback.params[0]; | ||
context.report({ | ||
node: argument, | ||
message: 'Illegal usage of test callback', | ||
fix(fixer) { | ||
const sourceCode = context.getSourceCode(); | ||
const body = callback.body; | ||
const firstBodyToken = sourceCode.getFirstToken(body); | ||
const lastBodyToken = sourceCode.getLastToken(body); | ||
const tokenBeforeArgument = sourceCode.getTokenBefore(argument); | ||
const tokenAfterArgument = sourceCode.getTokenAfter(argument); | ||
const argumentInParens = | ||
tokenBeforeArgument.value === '(' && | ||
tokenAfterArgument.value === ')'; | ||
|
||
let argumentFix = fixer.replaceText(argument, '()'); | ||
|
||
if (argumentInParens) { | ||
argumentFix = fixer.remove(argument); | ||
} | ||
|
||
let newCallback = argument.name; | ||
|
||
if (argumentInParens) { | ||
newCallback = `(${newCallback})`; | ||
} | ||
|
||
let beforeReplacement = `new Promise(${newCallback} => `; | ||
let afterReplacement = ')'; | ||
let replaceBefore = true; | ||
|
||
if (body.type === 'BlockStatement') { | ||
const keyword = callback.async ? 'await' : 'return'; | ||
|
||
beforeReplacement = `${keyword} ${beforeReplacement}{`; | ||
afterReplacement += '}'; | ||
replaceBefore = false; | ||
} | ||
|
||
return [ | ||
argumentFix, | ||
replaceBefore | ||
? fixer.insertTextBefore(firstBodyToken, beforeReplacement) | ||
: fixer.insertTextAfter(firstBodyToken, beforeReplacement), | ||
fixer.insertTextAfter(lastBodyToken, afterReplacement), | ||
]; | ||
}, | ||
}); | ||
}, | ||
}; | ||
}, | ||
}; |