From 42b120b8af63dda27c5ea665cc2c467bbadad0a2 Mon Sep 17 00:00:00 2001 From: makotot Date: Wed, 13 Jul 2022 22:50:29 +0900 Subject: [PATCH 01/11] feat: create max-expects --- docs/rules/max-expects.md | 74 ++++++++++ src/rules/__tests__/max-expects.test.ts | 180 ++++++++++++++++++++++++ src/rules/max-expects.ts | 89 ++++++++++++ 3 files changed, 343 insertions(+) create mode 100644 docs/rules/max-expects.md create mode 100644 src/rules/__tests__/max-expects.test.ts create mode 100644 src/rules/max-expects.ts diff --git a/docs/rules/max-expects.md b/docs/rules/max-expects.md new file mode 100644 index 000000000..3cbd1f02a --- /dev/null +++ b/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`. diff --git a/src/rules/__tests__/max-expects.test.ts b/src/rules/__tests__/max-expects.test.ts new file mode 100644 index 000000000..2cf43a60f --- /dev/null +++ b/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, + }, + ], + }, + ], +}); diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts new file mode 100644 index 000000000..3e8e6ac23 --- /dev/null +++ b/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', + recommended: false, + }, + messages: { + exceededMaxAssertion: + 'Too many assertion calls ({{ count }}). Maximum allowed is {{ max }}.', + }, + type: 'suggestion', + schema: [ + { + type: 'object', + properties: { + max: { + type: 'integer', + minimum: 0, + }, + }, + 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 || + isTypeOfJestFnCall(node.parent, context, ['test']); + + if (isTestFn) { + count = 0; + + return; + } + }; + const onFunctionExpressionExit = (node: FunctionExpression) => { + 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, + messageId: 'exceededMaxAssertion', + data: { count, max }, + }); + } + }, + }; + }, +}); From 910cfdd011986c31aedcb8f965243edf38f6f0a4 Mon Sep 17 00:00:00 2001 From: makotot Date: Wed, 13 Jul 2022 23:40:16 +0900 Subject: [PATCH 02/11] fix: fix test --- src/__tests__/__snapshots__/rules.test.ts.snap | 1 + src/__tests__/rules.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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) From 60ca6d2220707cfa733f813e642d870b02bc4f20 Mon Sep 17 00:00:00 2001 From: makotot Date: Wed, 13 Jul 2022 23:41:25 +0900 Subject: [PATCH 03/11] chore: update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 21bea0445..c3523ad72 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 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][] | | From 8bba714d62e447a1680ae4df59017c2fefd55a18 Mon Sep 17 00:00:00 2001 From: makotot Date: Fri, 15 Jul 2022 00:46:16 +0900 Subject: [PATCH 04/11] fix: set proper minimum --- src/rules/max-expects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts index 3e8e6ac23..1b59c9129 100644 --- a/src/rules/max-expects.ts +++ b/src/rules/max-expects.ts @@ -25,7 +25,7 @@ export default createRule({ properties: { max: { type: 'integer', - minimum: 0, + minimum: 1, }, }, additionalProperties: false, From b9e4ca486fe43e334ba3e8a869a0d6e7aba6668b Mon Sep 17 00:00:00 2001 From: makotot Date: Fri, 15 Jul 2022 00:48:37 +0900 Subject: [PATCH 05/11] refactor: remove unnecessary functions --- src/rules/max-expects.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts index 1b59c9129..577649fd2 100644 --- a/src/rules/max-expects.ts +++ b/src/rules/max-expects.ts @@ -51,27 +51,10 @@ export default createRule({ return; } }; - const onFunctionExpressionExit = (node: FunctionExpression) => { - 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; From fed2270a7567e8c489e757615a568b65386f6fe8 Mon Sep 17 00:00:00 2001 From: makotot Date: Fri, 15 Jul 2022 00:53:28 +0900 Subject: [PATCH 06/11] refactor: reduce if --- src/rules/max-expects.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts index 577649fd2..ef2b398ba 100644 --- a/src/rules/max-expects.ts +++ b/src/rules/max-expects.ts @@ -37,12 +37,8 @@ export default createRule({ let count = 0; const onFunctionExpressionEnter = (node: FunctionExpression) => { - if (!node?.parent) { - return; - } - const isTestFn = - node.parent.type !== AST_NODE_TYPES.CallExpression || + node.parent?.type !== AST_NODE_TYPES.CallExpression || isTypeOfJestFnCall(node.parent, context, ['test']); if (isTestFn) { From 29174fd4aae37610c9b869ec836d336cb7bd174f Mon Sep 17 00:00:00 2001 From: makotot Date: Fri, 15 Jul 2022 00:55:31 +0900 Subject: [PATCH 07/11] refactor: remove unnecessary type check --- src/rules/max-expects.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts index ef2b398ba..c82cf1898 100644 --- a/src/rules/max-expects.ts +++ b/src/rules/max-expects.ts @@ -55,9 +55,9 @@ export default createRule({ if (isExpectCall(node)) { count += 1; } - if (count > max && node.parent) { + if (count > max && node) { context.report({ - node: node.parent, + node, messageId: 'exceededMaxAssertion', data: { count, max }, }); From 79bb73484f6ce6c26df82c9a7ca726c864a84091 Mon Sep 17 00:00:00 2001 From: makotot Date: Fri, 15 Jul 2022 01:19:48 +0900 Subject: [PATCH 08/11] fix: incorrect error reported in case of multiple test blocks --- src/rules/__tests__/max-expects.test.ts | 104 ++++++++++++++++++++++++ src/rules/max-expects.ts | 7 +- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/rules/__tests__/max-expects.test.ts b/src/rules/__tests__/max-expects.test.ts index 2cf43a60f..f770dcfde 100644 --- a/src/rules/__tests__/max-expects.test.ts +++ b/src/rules/__tests__/max-expects.test.ts @@ -57,6 +57,38 @@ ruleTester.run('max-expects', rule, { 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', () => { @@ -156,6 +188,78 @@ ruleTester.run('max-expects', rule, { }, ], }, + { + 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', () => { diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts index c82cf1898..b1be4437f 100644 --- a/src/rules/max-expects.ts +++ b/src/rules/max-expects.ts @@ -52,9 +52,12 @@ export default createRule({ FunctionExpression: onFunctionExpressionEnter, ArrowFunctionExpression: onFunctionExpressionEnter, CallExpression(node) { - if (isExpectCall(node)) { - count += 1; + if (!isExpectCall(node)) { + return; } + + count += 1; + if (count > max && node) { context.report({ node, From d96cca7a682d639b072ea3097912178f90cbb610 Mon Sep 17 00:00:00 2001 From: makotot Date: Fri, 15 Jul 2022 01:23:17 +0900 Subject: [PATCH 09/11] chore: typo --- README.md | 2 +- docs/rules/max-expects.md | 2 +- src/rules/max-expects.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c3523ad72..ad88772d4 100644 --- a/README.md +++ b/README.md @@ -201,7 +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-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 index 3cbd1f02a..1efa59ae7 100644 --- a/docs/rules/max-expects.md +++ b/docs/rules/max-expects.md @@ -1,4 +1,4 @@ -# Enforces a maximum number of assertion calls in a test (`max-expects`) +# 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 diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts index b1be4437f..6634d818a 100644 --- a/src/rules/max-expects.ts +++ b/src/rules/max-expects.ts @@ -11,7 +11,7 @@ export default createRule({ meta: { docs: { category: 'Best Practices', - description: 'Enforces a maximum assertion calls in a test body', + description: 'Enforces a maximum number assertion calls in a test body', recommended: false, }, messages: { From 589b7d48f0abe0f70fe8d37d01f8c3eee5d17d0a Mon Sep 17 00:00:00 2001 From: makotot Date: Fri, 15 Jul 2022 01:57:49 +0900 Subject: [PATCH 10/11] refactor: remove unnecessary type check --- src/rules/max-expects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts index 6634d818a..cd9ee6220 100644 --- a/src/rules/max-expects.ts +++ b/src/rules/max-expects.ts @@ -58,7 +58,7 @@ export default createRule({ count += 1; - if (count > max && node) { + if (count > max) { context.report({ node, messageId: 'exceededMaxAssertion', From 146720853d4d16b623252ca9aed5c70aa97b8d51 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Fri, 15 Jul 2022 07:13:01 +1200 Subject: [PATCH 11/11] chore: remove unneeded return --- src/rules/max-expects.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts index cd9ee6220..0de46d24b 100644 --- a/src/rules/max-expects.ts +++ b/src/rules/max-expects.ts @@ -43,8 +43,6 @@ export default createRule({ if (isTestFn) { count = 0; - - return; } };