Skip to content

Commit

Permalink
Add new rule no-long-arrays-in-test-each (#3)
Browse files Browse the repository at this point in the history
* Update wording

* Add new rule `no-long-arrays-in-test-each`

* Add docs
  • Loading branch information
maks-rafalko authored Jul 6, 2024
1 parent a864739 commit 128d66d
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ To enable this configuration with `.eslintrc`, use the `extends` property:

| Name                             | Description | 💼 |
| :--------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | :- |
| [no-long-arrays-in-test-each](docs/rules/no-long-arrays-in-test-each.md) | Disallow mixing expectations for different variables between each other. ||
| [no-mixed-expectation-groups](docs/rules/no-mixed-expectation-groups.md) | Disallow mixing expectations for different variables between each other. ||
| [no-useless-matcher-to-be-defined](docs/rules/no-useless-matcher-to-be-defined.md) | Disallow using `.toBeDefined()` matcher when it is known that variable is always defined. ||
| [no-useless-matcher-to-be-null](docs/rules/no-useless-matcher-to-be-null.md) | Disallow using `.toBeNull()` when TypeScript types conflict with it. ||
Expand Down
102 changes: 102 additions & 0 deletions docs/rules/no-long-arrays-in-test-each.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Disallow mixing expectations for different variables between each other (`proper-tests/no-long-arrays-in-test-each`)

💼 This rule is enabled in the ✅ `recommended` config.

<!-- end auto-generated rule header -->

## Rule details

This rule disallows mixing expectations for different variables between each other.

The following code is considered errors:

```ts
test.each([
{
description: 'test case name #1',
inputValue: 'a',
expectedOutput: 'aa',
},
{
description: 'test case name #2',
inputValue: 'b',
expectedOutput: 'bb',
},
{
description: 'test case name #3',
inputValue: 'c',
expectedOutput: 'cc',
},
{
description: 'test case name #4',
inputValue: 'd',
expectedOutput: 'dd',
},
{
description: 'test case name #5',
inputValue: 'e',
expectedOutput: 'ee',
},
{
description: 'test case name #6',
inputValue: 'f',
expectedOutput: 'ff',
},
])('$description', ({ clientCountry, expectedPaymentMethod, processorName }) => {
// ...
});
```

Consider extracting such long arrays to a separate files with for example `.data.ts` postfix.

The following code is considered correct:

```ts
// some-service.data.ts
export type TestCase = Readonly<{
description: string;
inputValue: string;
expectedOutput: string;
}>;

export const testCases: TestCase[] = [
{
description: 'test case name #1',
inputValue: 'a',
expectedOutput: 'aa',
},
{
description: 'test case name #2',
inputValue: 'b',
expectedOutput: 'bb',
},
{
description: 'test case name #3',
inputValue: 'c',
expectedOutput: 'cc',
},
{
description: 'test case name #4',
inputValue: 'd',
expectedOutput: 'dd',
},
{
description: 'test case name #5',
inputValue: 'e',
expectedOutput: 'ee',
},
{
description: 'test case name #6',
inputValue: 'f',
expectedOutput: 'ff',
},
];
```

and now test is more readable:

```ts
test.each(testCases)('$description', ({ inputValue, expectedOutput }) => {
// ...
});
```
1 change: 1 addition & 0 deletions src/configs/recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export = {
'proper-tests/no-useless-matcher-to-be-defined': 'error',
'proper-tests/no-useless-matcher-to-be-null': 'error',
'proper-tests/no-mixed-expectation-groups': 'error',
'proper-tests/no-long-arrays-in-test-each': 'error',
},
} satisfies ClassicConfig.Config;
130 changes: 130 additions & 0 deletions src/custom-rules/no-long-arrays-in-test-each.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { RuleTester } from '@typescript-eslint/rule-tester';

import { noLongArraysInTestEach } from './no-long-arrays-in-test-each';

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

ruleTester.run('no-long-arrays-in-test-each', noLongArraysInTestEach, {
valid: [
{
name: 'less than default limit using "test"',
filename: 'app.e2e-spec.ts',
code: `test.each([{}])`,
},
{
name: 'less than default limit using "it"',
filename: 'app.e2e-spec.ts',
code: `it.each([{}])`,
},
{
name: 'with integers using "test"',
filename: 'app.e2e-spec.ts',
code: 'test.each([1, 2, 3, 4, 5, 6]);',
},
{
name: 'with integers using "it"',
filename: 'app.e2e-spec.ts',
code: 'it.each([1, 2, 3, 4, 5, 6]);',
},
{
name: 'with mixed integers and objects using "it"',
filename: 'app.e2e-spec.ts',
code: 'it.each([{}, {}, {}, {}, {}, 1, 2, 3, 4, 5]);',
},
{
name: 'with 6 objects when option is overridden using "it"',
filename: 'app.e2e-spec.ts',
options: [{ limit: 7 }],
code: 'it.each([{}, {}, {}, {}, {}, {}]);',
},
{
name: 'with 2 objects when option is overridden using "it"',
filename: 'app.e2e-spec.ts',
options: [{ limit: 4 }],
code: 'it.each([{}, {}]);',
},
{
name: 'string contains it.each',
filename: 'app.e2e-spec.ts',
code: '"it.each([{}, {}])";',
},
{
name: 'simple function call is executed',
filename: 'app.e2e-spec.ts',
code: 'each([{}, {}]);',
},
{
name: 'neither test nor it object is used',
filename: 'app.e2e-spec.ts',
code: 'array.each([{}, {}]);',
},
{
name: 'called function is not "each"',
filename: 'app.e2e-spec.ts',
code: 'test.every([{}, {}]);',
},
{
name: 'function argument is not an array but string',
filename: 'app.e2e-spec.ts',
code: 'test.each("string");',
},
{
name: 'not in e2e test file',
filename: 'non-e2e-test.ts',
code: 'it.each([{}, {}, {}, {}, {}]);',
},
],
invalid: [
{
name: 'when 6 objects are passed while 5 are allowed by default using "test.each"',
filename: 'app.e2e-spec.ts',
code: 'test.each([{}, {}, {}, {}, {}, {}])',
output: null,
errors: [
{
messageId: 'noLongArrays',
data: {
testFunctionName: 'test',
actualLength: 6,
limit: 5,
},
},
],
},
{
name: 'when 6 objects are passed while 5 are allowed by default using "it.each"',
filename: 'app.e2e-spec.ts',
code: 'it.each([{}, {}, {}, {}, {}, {}])',
output: null,
errors: [
{
messageId: 'noLongArrays',
data: {
testFunctionName: 'it',
actualLength: 6,
limit: 5,
},
},
],
},
{
name: 'when 2 objects are passed while 1 is allowed by passed option using "it.each"',
filename: 'app.e2e-spec.ts',
options: [{ limit: 1 }],
code: 'it.each([{}, {}])',
output: null,
errors: [
{
messageId: 'noLongArrays',
data: {
testFunctionName: 'it',
actualLength: 2,
limit: 1,
},
},
],
},
],
});
84 changes: 84 additions & 0 deletions src/custom-rules/no-long-arrays-in-test-each.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';

type MessageIds = 'noLongArrays';
type Options = [{ limit?: number }];

const DEFAULT_LIMIT = 5;

export const noLongArraysInTestEach = ESLintUtils.RuleCreator.withoutDocs<Options, MessageIds>({
create(context, options) {
return {
CallExpression(node) {
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
return;
}

if (
node.callee.object.type !== AST_NODE_TYPES.Identifier ||
!['test', 'it'].includes(node.callee.object.name)
) {
return;
}

if (node.callee.property.type !== AST_NODE_TYPES.Identifier || node.callee.property.name !== 'each') {
return;
}

const firstArgument = node.arguments[0];

if (firstArgument.type !== AST_NODE_TYPES.ArrayExpression) {
return;
}

const limit = options[0].limit || DEFAULT_LIMIT;
const elements = firstArgument.elements;

if (elements.length <= limit) {
return;
}

// eslint-disable-next-line @typescript-eslint/typedef
const allElementsAreObjects = elements.every(element => element?.type === AST_NODE_TYPES.ObjectExpression);

if (!allElementsAreObjects) {
return;
}

context.report({
node,
messageId: 'noLongArrays',
data: {
testFunctionName: node.callee.object.name,
actualLength: elements.length,
limit: limit,
},
});
},
};
},
meta: {
docs: {
description:
'Disallow using long arrays with objects inside `test.each()` or `it.each()`. Force moving them out of the file.',
},
messages: {
// eslint-disable-next-line max-len
noLongArrays:
'Move the array with objects out of the test file in `{{ testFunctionName }}.each()`. Array length is {{ actualLength }}, but the limit is {{ limit }} items.',
},
type: 'suggestion',
schema: [
{
type: 'object',
properties: {
limit: {
type: 'integer',
minimum: 1,
},
},
additionalProperties: false,
},
],
},
defaultOptions: [{ limit: DEFAULT_LIMIT }],
});
2 changes: 1 addition & 1 deletion src/custom-rules/no-mixed-expectation-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const noMixedExpectationGroups = ESLintUtils.RuleCreator.withoutDocs<Opti
},
messages: {
noMixedExpectationGroups:
'Expectation for variable "{{ variable }}" should be moved above to the same place where it is check for the first time. Do not mix expectations of different variables.',
'Expectation for variable "{{ variable }}" should be moved above to the same place where it is checked for the first time. Do not mix expectations of different variables.',
},
type: 'suggestion',
schema: [],
Expand Down
1 change: 1 addition & 0 deletions src/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('proper tests plugin', (): void => {
'no-useless-matcher-to-be-defined',
'no-useless-matcher-to-be-null',
'no-mixed-expectation-groups',
'no-long-arrays-in-test-each',
]);
});

Expand Down
1 change: 1 addition & 0 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export = {
'no-useless-matcher-to-be-defined': noUselessMatcherToBeDefined,
'no-useless-matcher-to-be-null': noUselessMatcherToBeNull,
'no-mixed-expectation-groups': noMixedExpectationGroups,
'no-long-arrays-in-test-each': noMixedExpectationGroups,
},
} satisfies Linter.Plugin;

0 comments on commit 128d66d

Please sign in to comment.