diff --git a/README.md b/README.md index 21bea0445..ad88772d4 100644 --- a/README.md +++ b/README.md @@ -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 assertion calls in a test body | | | | [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][] | | diff --git a/docs/rules/max-expects.md b/docs/rules/max-expects.md new file mode 100644 index 000000000..1efa59ae7 --- /dev/null +++ b/docs/rules/max-expects.md @@ -0,0 +1,74 @@ +# Enforces a maximum number assertion calls in a test body (`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`. diff --git a/src/__tests__/__snapshots__/rules.test.ts.snap b/src/__tests__/__snapshots__/rules.test.ts.snap index 80723e13b..4d9cbd30b 100644 --- a/src/__tests__/__snapshots__/rules.test.ts.snap +++ b/src/__tests__/__snapshots__/rules.test.ts.snap @@ -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", diff --git a/src/__tests__/rules.test.ts b/src/__tests__/rules.test.ts index 484887325..826bfb533 100644 --- a/src/__tests__/rules.test.ts +++ b/src/__tests__/rules.test.ts @@ -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) diff --git a/src/rules/__tests__/max-expects.test.ts b/src/rules/__tests__/max-expects.test.ts new file mode 100644 index 000000000..f770dcfde --- /dev/null +++ b/src/rules/__tests__/max-expects.test.ts @@ -0,0 +1,284 @@ +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(); + }); + `, + dedent` + describe('test', () => { + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + }); + `, + dedent` + test.each(['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(); + }); + test('should pass', () => { + 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(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + 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, + }, + { + messageId: 'exceededMaxAssertion', + line: 15, + column: 3, + }, + ], + }, + { + code: dedent` + describe('test', () => { + 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: 8, + column: 5, + }, + ], + }, + { + code: dedent` + test.each(['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` + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + options: [ + { + max: 1, + }, + ], + errors: [ + { + messageId: 'exceededMaxAssertion', + line: 3, + column: 3, + }, + ], + }, + ], +}); diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts new file mode 100644 index 000000000..0de46d24b --- /dev/null +++ b/src/rules/max-expects.ts @@ -0,0 +1,69 @@ +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 number assertion calls in a test body', + recommended: false, + }, + messages: { + exceededMaxAssertion: + 'Too many assertion calls ({{ count }}). Maximum allowed is {{ max }}.', + }, + type: 'suggestion', + schema: [ + { + type: 'object', + properties: { + max: { + type: 'integer', + minimum: 1, + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [{ max: 5 }], + create(context, [{ max }]) { + let count = 0; + + const onFunctionExpressionEnter = (node: FunctionExpression) => { + const isTestFn = + node.parent?.type !== AST_NODE_TYPES.CallExpression || + isTypeOfJestFnCall(node.parent, context, ['test']); + + if (isTestFn) { + count = 0; + } + }; + + return { + FunctionExpression: onFunctionExpressionEnter, + ArrowFunctionExpression: onFunctionExpressionEnter, + CallExpression(node) { + if (!isExpectCall(node)) { + return; + } + + count += 1; + + if (count > max) { + context.report({ + node, + messageId: 'exceededMaxAssertion', + data: { count, max }, + }); + } + }, + }; + }, +});