diff --git a/README.md b/README.md index c16b97ab7..3d819af90 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ for more information about extending configuration files. | [consistent-test-it][] | Enforce consistent test or it keyword | | ![fixable-green][] | | [expect-expect][] | Enforce assertion to be made in a test body | | | | [lowercase-name][] | Disallow capitalized test names | | ![fixable-green][] | +| [no-alias-methods][] | Disallow alias methods | | | | [no-disabled-tests][] | Disallow disabled tests | ![recommended][] | | | [no-focused-tests][] | Disallow focused tests | ![recommended][] | | | [no-hooks][] | Disallow setup and teardown hooks | | | @@ -111,6 +112,7 @@ for more information about extending configuration files. [consistent-test-it]: docs/rules/consistent-test-it.md [expect-expect]: docs/rules/expect-expect.md [lowercase-name]: docs/rules/lowercase-name.md +[no-alias-methods]: docs/rules/no-alias-methods.md [no-disabled-tests]: docs/rules/no-disabled-tests.md [no-focused-tests]: docs/rules/no-focused-tests.md [no-hooks]: docs/rules/no-hooks.md diff --git a/docs/rules/no-alias-methods.md b/docs/rules/no-alias-methods.md new file mode 100644 index 000000000..9439abe3c --- /dev/null +++ b/docs/rules/no-alias-methods.md @@ -0,0 +1,46 @@ +# Don't use alias methods (no-alias-methods) + +Several Jest methods have alias names, such as `toThrow` having the alias of +`toThrowError`. This rule ensures that only the canonical name as used in the +Jest documentation is used in the code. This makes it easier to search for all +occurrences of the method within code, and it ensures consistency among the +method names used. + +## Rule details + +This rule triggers a warning if the alias name, rather than the canonical name, +of a method is used. + +### Default configuration + +The following patterns are considered warnings: + +```js +expect(a).toBeCalled(); +expect(a).toBeCalledTimes(); +expect(a).toBeCalledWith(); +expect(a).lastCalledWith(); +expect(a).nthCalledWith(); +expect(a).toReturn(); +expect(a).toReturnTimes(); +expect(a).toReturnWith(); +expect(a).lastReturnedWith(); +expect(a).nthReturnedWith(); +expect(a).toThrowError(); +``` + +The following patterns are not considered warnings: + +```js +expect(a).toHaveBeenCalled(); +expect(a).toHaveBeenCalledTimes(); +expect(a).toHaveBeenCalledWith(); +expect(a).toHaveBeenLastCalledWith(); +expect(a).toHaveBeenNthCalledWith(); +expect(a).toHaveReturned(); +expect(a).toHaveReturnedTimes(); +expect(a).toHaveReturnedWith(); +expect(a).toHaveLastReturnedWith(); +expect(a).toHaveNthReturnedWith(); +expect(a).toThrow(); +``` diff --git a/index.js b/index.js index 0ae57b9e7..8c90da4d7 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,7 @@ const validExpectInPromise = require('./rules/valid-expect-in-promise'); const preferInlineSnapshots = require('./rules/prefer-inline-snapshots'); const preferStrictEqual = require('./rules/prefer-strict-equal'); const requireTothrowMessage = require('./rules/require-tothrow-message'); +const noAliasMethods = require('./rules/no-alias-methods'); const snapshotProcessor = require('./processors/snapshot-processor'); @@ -91,5 +92,6 @@ module.exports = { 'prefer-inline-snapshots': preferInlineSnapshots, 'prefer-strict-equal': preferStrictEqual, 'require-tothrow-message': requireTothrowMessage, + 'no-alias-methods': noAliasMethods, }, }; diff --git a/rules/__tests__/no-alias-methods.test.js b/rules/__tests__/no-alias-methods.test.js new file mode 100644 index 000000000..c8794dca1 --- /dev/null +++ b/rules/__tests__/no-alias-methods.test.js @@ -0,0 +1,157 @@ +'use strict'; + +const RuleTester = require('eslint').RuleTester; +const rule = require('../no-alias-methods'); + +const ruleTester = new RuleTester(); + +ruleTester.run('no-alias-methods', rule, { + valid: [ + 'expect(a).toHaveBeenCalled()', + 'expect(a).toHaveBeenCalledTimes()', + 'expect(a).toHaveBeenCalledWith()', + 'expect(a).toHaveBeenLastCalledWith()', + 'expect(a).toHaveBeenNthCalledWith()', + 'expect(a).toHaveReturned()', + 'expect(a).toHaveReturnedTimes()', + 'expect(a).toHaveReturnedWith()', + 'expect(a).toHaveLastReturnedWith()', + 'expect(a).toHaveNthReturnedWith()', + 'expect(a).toThrow()', + ], + + invalid: [ + { + code: 'expect(a).toBeCalled()', + errors: [ + { + message: + 'Replace toBeCalled() with its canonical name of toHaveBeenCalled()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toHaveBeenCalled()', + }, + { + code: 'expect(a).toBeCalledTimes()', + errors: [ + { + message: + 'Replace toBeCalledTimes() with its canonical name of toHaveBeenCalledTimes()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toHaveBeenCalledTimes()', + }, + { + code: 'expect(a).toBeCalledWith()', + errors: [ + { + message: + 'Replace toBeCalledWith() with its canonical name of toHaveBeenCalledWith()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toHaveBeenCalledWith()', + }, + { + code: 'expect(a).lastCalledWith()', + errors: [ + { + message: + 'Replace lastCalledWith() with its canonical name of toHaveBeenLastCalledWith()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toHaveBeenLastCalledWith()', + }, + { + code: 'expect(a).nthCalledWith()', + errors: [ + { + message: + 'Replace nthCalledWith() with its canonical name of toHaveBeenNthCalledWith()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toHaveBeenNthCalledWith()', + }, + { + code: 'expect(a).toReturn()', + errors: [ + { + message: + 'Replace toReturn() with its canonical name of toHaveReturned()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toHaveReturned()', + }, + { + code: 'expect(a).toReturnTimes()', + errors: [ + { + message: + 'Replace toReturnTimes() with its canonical name of toHaveReturnedTimes()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toHaveReturnedTimes()', + }, + { + code: 'expect(a).toReturnWith()', + errors: [ + { + message: + 'Replace toReturnWith() with its canonical name of toHaveReturnedWith()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toHaveReturnedWith()', + }, + { + code: 'expect(a).lastReturnedWith()', + errors: [ + { + message: + 'Replace lastReturnedWith() with its canonical name of toHaveLastReturnedWith()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toHaveLastReturnedWith()', + }, + { + code: 'expect(a).nthReturnedWith()', + errors: [ + { + message: + 'Replace nthReturnedWith() with its canonical name of toHaveNthReturnedWith()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toHaveNthReturnedWith()', + }, + { + code: 'expect(a).toThrowError()', + errors: [ + { + message: + 'Replace toThrowError() with its canonical name of toThrow()', + column: 11, + line: 1, + }, + ], + output: 'expect(a).toThrow()', + }, + ], +}); diff --git a/rules/no-alias-methods.js b/rules/no-alias-methods.js new file mode 100644 index 000000000..1b242c313 --- /dev/null +++ b/rules/no-alias-methods.js @@ -0,0 +1,57 @@ +'use strict'; + +const expectCase = require('./util').expectCase; +const getDocsUrl = require('./util').getDocsUrl; +const method = require('./util').method; + +module.exports = { + meta: { + docs: { + url: getDocsUrl(__filename), + }, + fixable: 'code', + }, + create(context) { + // The Jest methods which have aliases. The canonical name is the first + // index of each item. + const methodNames = [ + ['toHaveBeenCalled', 'toBeCalled'], + ['toHaveBeenCalledTimes', 'toBeCalledTimes'], + ['toHaveBeenCalledWith', 'toBeCalledWith'], + ['toHaveBeenLastCalledWith', 'lastCalledWith'], + ['toHaveBeenNthCalledWith', 'nthCalledWith'], + ['toHaveReturned', 'toReturn'], + ['toHaveReturnedTimes', 'toReturnTimes'], + ['toHaveReturnedWith', 'toReturnWith'], + ['toHaveLastReturnedWith', 'lastReturnedWith'], + ['toHaveNthReturnedWith', 'nthReturnedWith'], + ['toThrow', 'toThrowError'], + ]; + + return { + CallExpression(node) { + if (!expectCase(node)) { + return; + } + + // Check if the method used matches any of ours. + const propertyName = method(node) && method(node).name; + const methodItem = methodNames.find(item => item[1] === propertyName); + + if (methodItem) { + context.report({ + message: `Replace {{ replace }}() with its canonical name of {{ canonical }}()`, + data: { + replace: methodItem[1], + canonical: methodItem[0], + }, + node: method(node), + fix(fixer) { + return [fixer.replaceText(method(node), methodItem[0])]; + }, + }); + } + }, + }; + }, +};