diff --git a/README.md b/README.md index 269e908ec..c0f854db0 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ for more information about extending configuration files. | [consistent-test-it](docs/rules/consistent-test-it.md) | Enforce consistent test or it keyword | | ![fixable](https://img.shields.io/badge/-fixable-green.svg) | | [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | | | [no-focused-tests](docs/rules/no-focused-tests.md) | Disallow focused tests | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | | +| [no-hooks](docs/rules/no-hooks.md) | Disallow setup and teardown hooks | | | | [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | | | [no-large-snapshots](docs/rules/no-large-snapshots.md) | Disallow large snapshots | | | | [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) | diff --git a/docs/rules/no-hooks.md b/docs/rules/no-hooks.md new file mode 100644 index 000000000..3bdedd865 --- /dev/null +++ b/docs/rules/no-hooks.md @@ -0,0 +1,175 @@ +# Disallow setup and teardown hooks (no-hooks) + +Jest provides global functions for setup and teardown tasks, which are called +before/after each test case and each test suite. The use of these hooks promotes +shared state between tests. + +## Rule Details + +This rule reports for the following function calls: + +* `beforeAll` +* `beforeEach` +* `afterAll` +* `afterEach` + +Examples of **incorrect** code for this rule: + +```js +/* eslint jest/no-hooks: "error" */ + +function setupFoo(options) { + /* ... */ +} + +function setupBar(options) { + /* ... */ +} + +describe('foo', () => { + let foo; + + beforeEach(() => { + foo = setupFoo(); + }); + + afterEach(() => { + foo = null; + }); + + it('does something', () => { + expect(foo.doesSomething()).toBe(true); + }); + + describe('with bar', () => { + let bar; + + beforeEach(() => { + bar = setupBar(); + }); + + afterEach(() => { + bar = null; + }); + + it('does something with bar', () => { + expect(foo.doesSomething(bar)).toBe(true); + }); + }); +}); +``` + +Examples of **correct** code for this rule: + +```js +/* eslint jest/no-hooks: "error" */ + +function setupFoo(options) { + /* ... */ +} + +function setupBar(options) { + /* ... */ +} + +describe('foo', () => { + it('does something', () => { + const foo = setupFoo(); + expect(foo.doesSomething()).toBe(true); + }); + + it('does something with bar', () => { + const foo = setupFoo(); + const bar = setupBar(); + expect(foo.doesSomething(bar)).toBe(true); + }); +}); +``` + +## Options + +```json +{ + "jest/no-hooks": [ + "error", + { + "allow": ["afterEach", "afterAll"] + } + ] +} +``` + +### `allow` + +This array option whitelists setup and teardown hooks so that this rule does not +report their usage as being incorrect. There are four possible values: + +* `"beforeAll"` +* `"beforeEach"` +* `"afterAll"` +* `"afterEach"` + +By default, none of these options are enabled (the equivalent of +`{ "allow": [] }`). + +Examples of **incorrect** code for the `{ "allow": ["afterEach"] }` option: + +```js +/* eslint jest/no-hooks: ["error", { "allow": ["afterEach"] }] */ + +function setupFoo(options) { + /* ... */ +} + +let foo; + +beforeEach(() => { + foo = setupFoo(); +}); + +afterEach(() => { + jest.resetModules(); +}); + +test('foo does this', () => { + // ... +}); + +test('foo does that', () => { + // ... +}); +``` + +Examples of **correct** code for the `{ "allow": ["afterEach"] }` option: + +```js +/* eslint jest/no-hooks: ["error", { "allow": ["afterEach"] }] */ + +function setupFoo(options) { + /* ... */ +} + +afterEach(() => { + jest.resetModules(); +}); + +test('foo does this', () => { + const foo = setupFoo(); + // ... +}); + +test('foo does that', () => { + const foo = setupFoo(); + // ... +}); +``` + +## When Not To Use It + +If you prefer using the setup and teardown hooks provided by Jest, you can +safely disable this rule. + +## Further Reading + +* [Jest docs - Setup and Teardown](https://facebook.github.io/jest/docs/en/setup-teardown.html) +* [@jamiebuilds Twitter thread](https://twitter.com/jamiebuilds/status/954906997169664000) diff --git a/index.js b/index.js index 4d60cdf07..654a8aa11 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ const consistentTestIt = require('./rules/consistent-test-it'); const noDisabledTests = require('./rules/no-disabled-tests'); const noFocusedTests = require('./rules/no-focused-tests'); +const noHooks = require('./rules/no-hooks'); const noIdenticalTitle = require('./rules/no-identical-title'); const noLargeSnapshots = require('./rules/no-large-snapshots'); const preferToBeNull = require('./rules/prefer-to-be-null'); @@ -61,6 +62,7 @@ module.exports = { 'consistent-test-it': consistentTestIt, 'no-disabled-tests': noDisabledTests, 'no-focused-tests': noFocusedTests, + 'no-hooks': noHooks, 'no-identical-title': noIdenticalTitle, 'no-large-snapshots': noLargeSnapshots, 'prefer-to-be-null': preferToBeNull, diff --git a/rules/__tests__/no-hooks.test.js b/rules/__tests__/no-hooks.test.js new file mode 100644 index 000000000..52e160554 --- /dev/null +++ b/rules/__tests__/no-hooks.test.js @@ -0,0 +1,44 @@ +'use strict'; + +const RuleTester = require('eslint').RuleTester; +const rules = require('../..').rules; +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 6, + }, +}); + +ruleTester.run('no-hooks', rules['no-hooks'], { + valid: [ + 'test("foo")', + 'describe("foo", () => { it("bar") })', + 'test("foo", () => { expect(subject.beforeEach()).toBe(true) })', + { + code: 'afterEach(() => {}); afterAll(() => {});', + options: [{ allow: ['afterEach', 'afterAll'] }], + }, + ], + invalid: [ + { + code: 'beforeAll(() => {})', + errors: [{ message: "Unexpected 'beforeAll' hook" }], + }, + { + code: 'beforeEach(() => {})', + errors: [{ message: "Unexpected 'beforeEach' hook" }], + }, + { + code: 'afterAll(() => {})', + errors: [{ message: "Unexpected 'afterAll' hook" }], + }, + { + code: 'afterEach(() => {})', + errors: [{ message: "Unexpected 'afterEach' hook" }], + }, + { + code: 'beforeEach(() => {}); afterEach(() => { jest.resetModules() });', + options: [{ allow: ['afterEach'] }], + errors: [{ message: "Unexpected 'beforeEach' hook" }], + }, + ], +}); diff --git a/rules/no-hooks.js b/rules/no-hooks.js new file mode 100644 index 000000000..85668fa06 --- /dev/null +++ b/rules/no-hooks.js @@ -0,0 +1,52 @@ +'use strict'; + +module.exports = { + meta: { + docs: { + url: + 'https://github.com/jest-community/eslint-plugin-jest/blob/master/docs/rules/no-hooks.md', + }, + }, + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + contains: ['beforeAll', 'beforeEach', 'afterAll', 'afterEach'], + }, + }, + additionalProperties: false, + }, + ], + create(context) { + const testHookNames = Object.assign(Object.create(null), { + beforeAll: true, + beforeEach: true, + afterAll: true, + afterEach: true, + }); + + const whitelistedHookNames = ( + context.options[0] || { allow: [] } + ).allow.reduce((hashMap, value) => { + hashMap[value] = true; + return hashMap; + }, Object.create(null)); + + const isHook = node => testHookNames[node.callee.name]; + const isWhitelisted = node => whitelistedHookNames[node.callee.name]; + + return { + CallExpression(node) { + if (isHook(node) && !isWhitelisted(node)) { + context.report({ + node, + message: "Unexpected '{{ hookName }}' hook", + data: { hookName: node.callee.name }, + }); + } + }, + }; + }, +};