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 prefer-each rule #1222

Merged
merged 1 commit into from Aug 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -226,6 +226,7 @@ installations requiring long-term consistency.
| [no-test-return-statement](docs/rules/no-test-return-statement.md) | Disallow explicitly returning from tests | | |
| [prefer-called-with](docs/rules/prefer-called-with.md) | Suggest using `toBeCalledWith()` or `toHaveBeenCalledWith()` | | |
| [prefer-comparison-matcher](docs/rules/prefer-comparison-matcher.md) | Suggest using the built-in comparison matchers | | ![fixable][] |
| [prefer-each](docs/rules/prefer-each.md) | Prefer using `.each` rather than manual loops | | |
| [prefer-equality-matcher](docs/rules/prefer-equality-matcher.md) | Suggest using the built-in equality matchers | | ![suggest][] |
| [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
54 changes: 54 additions & 0 deletions docs/rules/prefer-each.md
@@ -0,0 +1,54 @@
# Prefer using `.each` rather than manual loops (`prefer-each`)

Reports where you might be able to use `.each` instead of native loops.

## Rule details

This rule triggers a warning if you use test case functions like `describe`,
`test`, and `it`, in a native loop - generally you should be able to use `.each`
instead which gives better output and makes it easier to run specific cases.

Examples of **incorrect** code for this rule:

```js
for (const number of getNumbers()) {
it('is greater than five', function () {
expect(number).toBeGreaterThan(5);
});
}

for (const [input, expected] of data) {
beforeEach(() => setupSomething(input));

test(`results in ${expected}`, () => {
expect(doSomething()).toBe(expected);
});
}
```

Examples of **correct** code for this rule:

```js
it.each(getNumbers())(
'only returns numbers that are greater than seven',
number => {
expect(number).toBeGreaterThan(7);
},
);

describe.each(data)('when input is %s', ([input, expected]) => {
beforeEach(() => setupSomething(input));

test(`results in ${expected}`, () => {
expect(doSomething()).toBe(expected);
});
});

// we don't warn on loops _in_ test functions because those typically involve
// complex setup that is better done in the test function itself
it('returns numbers that are greater than five', () => {
for (const number of getNumbers()) {
expect(number).toBeGreaterThan(5);
}
});
```
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/rules.test.ts.snap
Expand Up @@ -37,6 +37,7 @@ Object {
"jest/no-test-return-statement": "error",
"jest/prefer-called-with": "error",
"jest/prefer-comparison-matcher": "error",
"jest/prefer-each": "error",
"jest/prefer-equality-matcher": "error",
"jest/prefer-expect-assertions": "error",
"jest/prefer-expect-resolves": "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 = 50;
const numberOfRules = 51;
const ruleNames = Object.keys(plugin.rules);
const deprecatedRules = Object.entries(plugin.rules)
.filter(([, rule]) => rule.meta.deprecated)
Expand Down
310 changes: 310 additions & 0 deletions src/rules/__tests__/prefer-each.test.ts
@@ -0,0 +1,310 @@
import { TSESLint } from '@typescript-eslint/utils';
import dedent from 'dedent';
import rule from '../prefer-each';
import { espreeParser } from './test-utils';

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

ruleTester.run('prefer-each', rule, {
valid: [
'it("is true", () => { expect(true).toBe(false) });',
dedent`
it.each(getNumbers())("only returns numbers that are greater than seven", number => {
expect(number).toBeGreaterThan(7);
});
`,
// while these cases could be done with .each, it's reasonable to have more
// complex cases that would not look good in .each, so we consider this valid
dedent`
it("returns numbers that are greater than five", function () {
for (const number of getNumbers()) {
expect(number).toBeGreaterThan(5);
}
});
`,
dedent`
it("returns things that are less than ten", function () {
for (const thing in things) {
expect(thing).toBeLessThan(10);
}
});
`,
dedent`
it("only returns numbers that are greater than seven", function () {
const numbers = getNumbers();

for (let i = 0; i < numbers.length; i++) {
expect(numbers[i]).toBeGreaterThan(7);
}
});
`,
],
invalid: [
{
code: dedent`
for (const [input, expected] of data) {
it(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}
`,
errors: [
{
data: { fn: 'it' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
for (const [input, expected] of data) {
describe(\`when the input is $\{input}\`, () => {
it(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
});
}
`,
errors: [
{
data: { fn: 'describe' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
for (const [input, expected] of data) {
describe(\`when the input is $\{input}\`, () => {
it(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
});
}

for (const [input, expected] of data) {
it.skip(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}
`,
errors: [
{
data: { fn: 'describe' },
messageId: 'preferEach',
},
{
data: { fn: 'it' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
for (const [input, expected] of data) {
it.skip(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}
`,
errors: [
{
data: { fn: 'it' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
it('is true', () => {
expect(true).toBe(false);
});

for (const [input, expected] of data) {
it.skip(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}
`,
errors: [
{
data: { fn: 'it' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
for (const [input, expected] of data) {
it.skip(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}

it('is true', () => {
expect(true).toBe(false);
});
`,
errors: [
{
data: { fn: 'it' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
it('is true', () => {
expect(true).toBe(false);
});

for (const [input, expected] of data) {
it.skip(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}

it('is true', () => {
expect(true).toBe(false);
});
`,
errors: [
{
data: { fn: 'it' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
for (const [input, expected] of data) {
it(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});

it(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}
`,
errors: [
{
data: { fn: 'describe' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
for (const [input, expected] of data) {
it(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}

for (const [input, expected] of data) {
it(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}
`,
errors: [
{
data: { fn: 'it' },
messageId: 'preferEach',
},
{
data: { fn: 'it' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
for (const [input, expected] of data) {
it(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}

for (const [input, expected] of data) {
test(\`results in $\{expected}\`, () => {
expect(fn(input)).toBe(expected)
});
}
`,
errors: [
{
data: { fn: 'it' },
messageId: 'preferEach',
},
{
data: { fn: 'it' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
for (const [input, expected] of data) {
beforeEach(() => setupSomething(input));

test(\`results in $\{expected}\`, () => {
expect(doSomething()).toBe(expected)
});
}
`,
errors: [
{
data: { fn: 'describe' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
for (const [input, expected] of data) {
it("only returns numbers that are greater than seven", function () {
const numbers = getNumbers(input);

for (let i = 0; i < numbers.length; i++) {
expect(numbers[i]).toBeGreaterThan(7);
}
});
}
`,
errors: [
{
data: { fn: 'it' },
messageId: 'preferEach',
},
],
},
{
code: dedent`
for (const [input, expected] of data) {
beforeEach(() => setupSomething(input));

it("only returns numbers that are greater than seven", function () {
const numbers = getNumbers();

for (let i = 0; i < numbers.length; i++) {
expect(numbers[i]).toBeGreaterThan(7);
}
});
}
`,
errors: [
{
data: { fn: 'describe' },
messageId: 'preferEach',
},
],
},
],
});