Skip to content

Commit

Permalink
feat(rule): add no-hooks rule (#74)
Browse files Browse the repository at this point in the history
Resolves #72
  • Loading branch information
macklinu authored and SimenB committed Feb 12, 2018
1 parent 38f8ca0 commit 178744b
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 0 deletions.
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 },
});
}
},
};
},
};

0 comments on commit 178744b

Please # to comment.