Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: create max-expects rule #1166

Merged
merged 11 commits into from Jul 14, 2022
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -201,6 +201,7 @@ installations requiring long-term consistency.
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------- | ---------------- | ------------ |
| [consistent-test-it](docs/rules/consistent-test-it.md) | Have control over `test` and `it` usages | | ![fixable][] |
| [expect-expect](docs/rules/expect-expect.md) | Enforce assertion to be made in a test body | ![recommended][] | |
| [max-expects](docs/rules/max-expects.md) | Enforces a maximum number of assertion calls in a test | | |
| [max-nested-describe](docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | | |
| [no-alias-methods](docs/rules/no-alias-methods.md) | Disallow alias methods | ![style][] | ![fixable][] |
| [no-commented-out-tests](docs/rules/no-commented-out-tests.md) | Disallow commented out tests | ![recommended][] | |
Expand Down
74 changes: 74 additions & 0 deletions docs/rules/max-expects.md
@@ -0,0 +1,74 @@
# Enforces a maximum number of assertion calls in a test (`max-expects`)

As more assertions are made, there is a possible tendency for the test to be
more likely to mix multiple objectives. To avoid this, this rule reports when
the maximum number of assertions is exceeded.

## Rule Details

This rule enforces a maximum number of `expect()` calls.

The following patterns are considered warnings (with the default option of
`{ "max": 5 } `):

```js
test('should not pass', () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});

it('should not pass', () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});
```

The following patterns are **not** considered warnings (with the default option
of `{ "max": 5 } `):

```js
test('shout pass');

test('shout pass', () => {});

test.skip('shout pass', () => {});

test('should pass', function () {
expect(true).toBeDefined();
});

test('should pass', () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});
```

## Options

```json
{
"jest/max-expects": [
"error",
{
"max": 5
}
]
}
```

### `max`

Enforces a maximum number of `expect()`.

This has a default value of `5`.
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/rules.test.ts.snap
Expand Up @@ -12,6 +12,7 @@ Object {
"rules": Object {
"jest/consistent-test-it": "error",
"jest/expect-expect": "error",
"jest/max-expects": "error",
"jest/max-nested-describe": "error",
"jest/no-alias-methods": "error",
"jest/no-commented-out-tests": "error",
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/rules.test.ts
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
180 changes: 180 additions & 0 deletions src/rules/__tests__/max-expects.test.ts
@@ -0,0 +1,180 @@
import { TSESLint } from '@typescript-eslint/utils';
import dedent from 'dedent';
import rule from '../max-expects';
import { espreeParser } from './test-utils';

const ruleTester = new TSESLint.RuleTester({
parser: espreeParser,
parserOptions: {
ecmaVersion: 2017,
},
});

ruleTester.run('max-expects', rule, {
valid: [
`test('should pass')`,
`test('should pass', () => {})`,
`test.skip('should pass', () => {})`,
dedent`
test('should pass', function () {
expect(true).toBeDefined();
});
`,
dedent`
test('should pass', () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});
`,
dedent`
test('should pass', () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
// expect(true).toBeDefined();
});
`,
dedent`
it('should pass', () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});
`,
dedent`
test('should pass', async () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});
`,
{
code: dedent`
test('should pass', () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});
`,
options: [
{
max: 10,
},
],
},
],
invalid: [
{
code: dedent`
test('should not pass', function () {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});
`,
errors: [
{
messageId: 'exceededMaxAssertion',
line: 7,
column: 3,
},
],
},
{
code: dedent`
test('should not pass', () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});
`,
errors: [
{
messageId: 'exceededMaxAssertion',
line: 7,
column: 3,
},
],
},
{
code: dedent`
it('should not pass', () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});
`,
errors: [
{
messageId: 'exceededMaxAssertion',
line: 7,
column: 3,
},
],
},
{
code: dedent`
it('should not pass', async () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
expect(true).toBeDefined();
});
`,
errors: [
{
messageId: 'exceededMaxAssertion',
line: 7,
column: 3,
},
],
},
{
code: dedent`
test('should not pass', () => {
expect(true).toBeDefined();
expect(true).toBeDefined();
});
`,
options: [
{
max: 1,
},
],
errors: [
{
messageId: 'exceededMaxAssertion',
line: 3,
column: 3,
},
],
},
],
});
89 changes: 89 additions & 0 deletions src/rules/max-expects.ts
@@ -0,0 +1,89 @@
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import {
FunctionExpression,
createRule,
isExpectCall,
isTypeOfJestFnCall,
} from './utils';

export default createRule({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Enforces a maximum assertion calls in a test body',
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
recommended: false,
},
messages: {
exceededMaxAssertion:
'Too many assertion calls ({{ count }}). Maximum allowed is {{ max }}.',
},
type: 'suggestion',
schema: [
{
type: 'object',
properties: {
max: {
type: 'integer',
minimum: 0,
makotot marked this conversation as resolved.
Show resolved Hide resolved
},
},
additionalProperties: false,
},
],
},
defaultOptions: [{ max: 5 }],
create(context, [{ max }]) {
let count = 0;

const onFunctionExpressionEnter = (node: FunctionExpression) => {
if (!node?.parent) {
return;
}

const isTestFn =
node.parent.type !== AST_NODE_TYPES.CallExpression ||
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
isTypeOfJestFnCall(node.parent, context, ['test']);

if (isTestFn) {
count = 0;

return;
}
};
const onFunctionExpressionExit = (node: FunctionExpression) => {
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
if (!node?.parent) {
return;
}

const isTestFn =
node.parent.type !== AST_NODE_TYPES.CallExpression ||
isTypeOfJestFnCall(node.parent, context, ['test']);

if (isTestFn) {
count = count - 1;

return;
}
};

return {
FunctionExpression: onFunctionExpressionEnter,
'FunctionExpression:exit': onFunctionExpressionExit,
ArrowFunctionExpression: onFunctionExpressionEnter,
'ArrowFunctionExpression:exit': onFunctionExpressionExit,
CallExpression(node) {
if (isExpectCall(node)) {
count += 1;
}
if (count > max && node.parent) {
context.report({
node: node.parent,
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
messageId: 'exceededMaxAssertion',
data: { count, max },
});
}
},
};
},
});