Skip to content

Commit

Permalink
feat: create no-unhooked-function-calls rule
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath committed Oct 9, 2021
1 parent 7a49c58 commit 175f0a4
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ installations requiring long-term consistency.
| [no-standalone-expect](docs/rules/no-standalone-expect.md) | Disallow using `expect` outside of `it` or `test` blocks | ![recommended][] | |
| [no-test-prefixes](docs/rules/no-test-prefixes.md) | Use `.only` and `.skip` over `f` and `x` | ![recommended][] | ![fixable][] |
| [no-test-return-statement](docs/rules/no-test-return-statement.md) | Disallow explicitly returning from tests | | |
| [no-unhooked-function-calls](docs/rules/no-unhooked-function-calls.md) | Checks for function calls within describes that are not in a hook | | |
| [prefer-called-with](docs/rules/prefer-called-with.md) | Suggest using `toBeCalledWith()` or `toHaveBeenCalledWith()` | | |
| [prefer-expect-assertions](docs/rules/prefer-expect-assertions.md) | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | ![suggest][] |
| [prefer-expect-resolves](docs/rules/prefer-expect-resolves.md) | Prefer `await expect(...).resolves` over `expect(await ...)` syntax | | ![fixable][] |
Expand Down
59 changes: 59 additions & 0 deletions docs/rules/no-unhooked-function-calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Checks for function calls within describes that are not in a hook (`no-unhooked-function-calls`)

Often while writing tests you have some setup work that needs to happen before
tests run, and you have some finishing work that needs to happen after tests
run. Jest provides helper functions to handle this.

It's common when writing tests to need to perform setup work that needs to
happen before tests run, and finishing work after tests run.

Because Jest executes all `describe` handlers in a test file _before_ it
executes any of the actual tests, it's important to ensure setup and teardown
work is done inside `before*` and `after*` handlers respectively, rather than
inside the `describe` blocks.

## Rule details

This rule flags any function calls within test files that are directly within
the body of a `describe`, and suggests wrapping them in one of the four
lifecycle hooks.

The following patterns are considered warnings:

```js
describe('cities', () => {
initializeCityDatabase();

test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});

clearCityDatabase();
});
```

The following patterns are **not** considered warnings:

```js
describe('cities', () => {
beforeEach(() => {
initializeCityDatabase();
});

test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});

afterEach(() => {
clearCityDatabase();
});
});
```
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/rules.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Object {
"jest/no-standalone-expect": "error",
"jest/no-test-prefixes": "error",
"jest/no-test-return-statement": "error",
"jest/no-unhooked-function-calls": "error",
"jest/prefer-called-with": "error",
"jest/prefer-expect-assertions": "error",
"jest/prefer-expect-resolves": "error",
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { existsSync } from 'fs';
import { resolve } from 'path';
import plugin from '../';

const numberOfRules = 48;
const numberOfRules = 49;
const ruleNames = Object.keys(plugin.rules);
const deprecatedRules = Object.entries(plugin.rules)
.filter(([, rule]) => rule.meta.deprecated)
Expand Down
146 changes: 146 additions & 0 deletions src/rules/__tests__/no-unhooked-function-calls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { TSESLint } from '@typescript-eslint/experimental-utils';
import dedent from 'dedent';
import resolveFrom from 'resolve-from';
import rule from '../no-unhooked-function-calls';

const ruleTester = new TSESLint.RuleTester({
parser: resolveFrom(require.resolve('eslint'), 'espree'),
parserOptions: {
ecmaVersion: 2017,
},
});

ruleTester.run('no-unhooked-function-calls', rule, {
valid: [
dedent`
test('it', () => {
//
});
`,
dedent`
describe('some tests', () => {
it('is true', () => {
expect(true).toBe(true);
});
});
`,
dedent`
describe('some tests', () => {
it('is true', () => {
expect(true).toBe(true);
});
describe('more tests', () => {
it('is false', () => {
expect(true).toBe(false);
});
});
});
`,
dedent`
describe('some tests', () => {
let consoleLogSpy;
beforeEach(() => {
consoleLogSpy = jest.spyOn(console, 'log');
});
it('prints a message', () => {
printMessage('hello world');
expect(consoleLogSpy).toHaveBeenCalledWith('hello world');
});
});
`,
dedent`
describe('some tests', () => {
beforeEach(() => {
setup();
});
});
`,
dedent`
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
`,
dedent`
describe('cities', () => {
beforeEach(() => {
initializeCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
afterEach(() => {
clearCityDatabase();
});
});
`,
],
invalid: [
{
code: dedent`
describe('some tests', () => {
setup();
});
`,
errors: [
{
messageId: 'useHook',
line: 2,
column: 3,
},
],
},
{
code: dedent`
describe('some tests', () => {
setup();
it('is true', () => {
expect(true).toBe(true);
});
describe('more tests', () => {
setup();
it('is false', () => {
expect(true).toBe(false);
});
});
});
`,
errors: [
{
messageId: 'useHook',
line: 2,
column: 3,
},
{
messageId: 'useHook',
line: 9,
column: 5,
},
],
},
],
});
64 changes: 64 additions & 0 deletions src/rules/no-unhooked-function-calls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
import {
createRule,
isDescribeCall,
isFunction,
isHook,
isTestCaseCall,
} from './utils';

export default createRule({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description:
'Checks for function calls within describes that are not in a hook',
recommended: false,
},
messages: {
useHook: 'This should be done within a hook',
},
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
if (!isDescribeCall(node) || node.arguments.length < 2) {
return;
}

const [, testFn] = node.arguments;

if (
!isFunction(testFn) ||
testFn.body.type !== AST_NODE_TYPES.BlockStatement
) {
return;
}

for (const nod of testFn.body.body) {
if (
nod.type === AST_NODE_TYPES.ExpressionStatement &&
nod.expression.type === AST_NODE_TYPES.CallExpression
) {
if (
isDescribeCall(nod.expression) ||
isTestCaseCall(nod.expression) ||
isHook(nod.expression)
) {
return;
}

context.report({
node: nod.expression,
messageId: 'useHook',
});
}
}
},
};
},
});

0 comments on commit 175f0a4

Please sign in to comment.