Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(rule): add no-hooks rule #74

Merged
merged 1 commit into from
Feb 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
175 changes: 175 additions & 0 deletions docs/rules/no-hooks.md
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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,
Expand Down
44 changes: 44 additions & 0 deletions rules/__tests__/no-hooks.test.js
Original file line number Diff line number Diff line change
@@ -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" }],
},
],
});
52 changes: 52 additions & 0 deletions rules/no-hooks.js
Original file line number Diff line number Diff line change
@@ -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 },
});
}
},
};
},
};