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(rules): add require-top-level-describe rule #407

Merged
merged 5 commits into from Aug 29, 2019
Merged
Show file tree
Hide file tree
Changes from 4 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
82 changes: 42 additions & 40 deletions README.md
Expand Up @@ -104,46 +104,47 @@ installations requiring long-term consistency.

## Rules

| Rule | Description | Recommended | Fixable |
| ---------------------------- | ----------------------------------------------------------------- | ---------------- | ------------------- |
| [consistent-test-it][] | Enforce consistent test or it keyword | | ![fixable-green][] |
| [expect-expect][] | Enforce assertion to be made in a test body | | |
| [lowercase-name][] | Disallow capitalized test names | | ![fixable-green][] |
| [no-alias-methods][] | Disallow alias methods | ![recommended][] | ![fixable-green][] |
| [no-commented-out-tests][] | Disallow commented out tests | | |
| [no-disabled-tests][] | Disallow disabled tests | ![recommended][] | |
| [no-duplicate-hooks][] | Disallow duplicate hooks within a `describe` block | | |
| [no-empty-title][] | Disallow empty titles | | |
| [no-expect-resolves][] | Disallow using `expect().resolves` | | |
| [no-export][] | Disallow export from test files | | |
| [no-focused-tests][] | Disallow focused tests | ![recommended][] | |
| [no-hooks][] | Disallow setup and teardown hooks | | |
| [no-identical-title][] | Disallow identical titles | ![recommended][] | |
| [no-if][] | Disallow conditional logic | | |
| [no-jasmine-globals][] | Disallow Jasmine globals | ![recommended][] | ![fixable-yellow][] |
| [no-jest-import][] | Disallow importing `jest` | ![recommended][] | |
| [no-large-snapshots][] | Disallow large snapshots | | |
| [no-mocks-import][] | Disallow manually importing from `__mocks__` | | |
| [no-standalone-expect][] | Prevents `expect` statements outside of a `test` or `it` block | | |
| [no-test-callback][] | Using a callback in asynchronous tests | | ![fixable-green][] |
| [no-test-prefixes][] | Disallow using `f` & `x` prefixes to define focused/skipped tests | ![recommended][] | ![fixable-green][] |
| [no-test-return-statement][] | Disallow explicitly returning from tests | | |
| [no-truthy-falsy][] | Disallow using `toBeTruthy()` & `toBeFalsy()` | | |
| [no-try-expect][] | Prevent `catch` assertions in tests | | |
| [prefer-called-with][] | Suggest using `toBeCalledWith()` OR `toHaveBeenCalledWith()` | | |
| [prefer-expect-assertions][] | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | |
| [prefer-inline-snapshots][] | Suggest using `toMatchInlineSnapshot()` | | ![fixable-green][] |
| [prefer-spy-on][] | Suggest using `jest.spyOn()` | | ![fixable-green][] |
| [prefer-strict-equal][] | Suggest using `toStrictEqual()` | | ![fixable-green][] |
| [prefer-to-be-null][] | Suggest using `toBeNull()` | | ![fixable-green][] |
| [prefer-to-be-undefined][] | Suggest using `toBeUndefined()` | | ![fixable-green][] |
| [prefer-to-contain][] | Suggest using `toContain()` | | ![fixable-green][] |
| [prefer-to-have-length][] | Suggest using `toHaveLength()` | | ![fixable-green][] |
| [prefer-todo][] | Suggest using `test.todo()` | | ![fixable-green][] |
| [require-tothrow-message][] | Require that `toThrow()` and `toThrowError` includes a message | | |
| [valid-describe][] | Enforce valid `describe()` callback | ![recommended][] | |
| [valid-expect-in-promise][] | Enforce having return statement when testing with promises | ![recommended][] | |
| [valid-expect][] | Enforce valid `expect()` usage | ![recommended][] | |
| Rule | Description | Recommended | Fixable |
| ------------------------------ | ----------------------------------------------------------------- | ---------------- | ------------------- |
| [consistent-test-it][] | Enforce consistent test or it keyword | | ![fixable-green][] |
| [expect-expect][] | Enforce assertion to be made in a test body | | |
| [lowercase-name][] | Disallow capitalized test names | | ![fixable-green][] |
| [no-alias-methods][] | Disallow alias methods | ![recommended][] | ![fixable-green][] |
| [no-commented-out-tests][] | Disallow commented out tests | | |
| [no-disabled-tests][] | Disallow disabled tests | ![recommended][] | |
| [no-duplicate-hooks][] | Disallow duplicate hooks within a `describe` block | | |
| [no-empty-title][] | Disallow empty titles | | |
| [no-expect-resolves][] | Disallow using `expect().resolves` | | |
| [no-export][] | Disallow export from test files | | |
| [no-focused-tests][] | Disallow focused tests | ![recommended][] | |
| [no-hooks][] | Disallow setup and teardown hooks | | |
| [no-identical-title][] | Disallow identical titles | ![recommended][] | |
| [no-if][] | Disallow conditional logic | | |
| [no-jasmine-globals][] | Disallow Jasmine globals | ![recommended][] | ![fixable-yellow][] |
| [no-jest-import][] | Disallow importing `jest` | ![recommended][] | |
| [no-large-snapshots][] | Disallow large snapshots | | |
| [no-mocks-import][] | Disallow manually importing from `__mocks__` | | |
| [no-standalone-expect][] | Prevents `expect` statements outside of a `test` or `it` block | | |
| [no-test-callback][] | Using a callback in asynchronous tests | | ![fixable-green][] |
| [no-test-prefixes][] | Disallow using `f` & `x` prefixes to define focused/skipped tests | ![recommended][] | ![fixable-green][] |
| [no-test-return-statement][] | Disallow explicitly returning from tests | | |
| [no-truthy-falsy][] | Disallow using `toBeTruthy()` & `toBeFalsy()` | | |
| [no-try-expect][] | Prevent `catch` assertions in tests | | |
| [prefer-called-with][] | Suggest using `toBeCalledWith()` OR `toHaveBeenCalledWith()` | | |
| [prefer-expect-assertions][] | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | |
| [prefer-inline-snapshots][] | Suggest using `toMatchInlineSnapshot()` | | ![fixable-green][] |
| [prefer-spy-on][] | Suggest using `jest.spyOn()` | | ![fixable-green][] |
| [prefer-strict-equal][] | Suggest using `toStrictEqual()` | | ![fixable-green][] |
| [prefer-to-be-null][] | Suggest using `toBeNull()` | | ![fixable-green][] |
| [prefer-to-be-undefined][] | Suggest using `toBeUndefined()` | | ![fixable-green][] |
| [prefer-to-contain][] | Suggest using `toContain()` | | ![fixable-green][] |
| [prefer-to-have-length][] | Suggest using `toHaveLength()` | | ![fixable-green][] |
| [prefer-todo][] | Suggest using `test.todo()` | | ![fixable-green][] |
| [require-top-level-describe][] | Require a top-level `describe` block | | |
| [require-tothrow-message][] | Require that `toThrow()` and `toThrowError` includes a message | | |
| [valid-describe][] | Enforce valid `describe()` callback | ![recommended][] | |
| [valid-expect-in-promise][] | Enforce having return statement when testing with promises | ![recommended][] | |
| [valid-expect][] | Enforce valid `expect()` usage | ![recommended][] | |

## Credit

Expand Down Expand Up @@ -193,6 +194,7 @@ https://github.com/dangreenisrael/eslint-plugin-jest-formatting
[prefer-to-contain]: docs/rules/prefer-to-contain.md
[prefer-to-have-length]: docs/rules/prefer-to-have-length.md
[prefer-todo]: docs/rules/prefer-todo.md
[require-top-level-describe]: docs/rules/require-top-level-describe.md
[require-tothrow-message]: docs/rules/require-tothrow-message.md
[valid-describe]: docs/rules/valid-describe.md
[valid-expect-in-promise]: docs/rules/valid-expect-in-promise.md
Expand Down
52 changes: 52 additions & 0 deletions docs/rules/require-top-level-describe.md
@@ -0,0 +1,52 @@
# Require top-level describe block (require-top-level-describe)

Jest allows you to organise your test files the way you want it. However, the
more your codebase grows, the more it becomes hard to navigate in your test
files. This rule makes sure that you provide at least a top-level describe block
in your test file.

## Rule Details

This rule triggers a warning if a test case (`test` and `it`) or a hook
(`beforeAll`, `beforeEach`, `afterEach`, `afterAll`) is not located in a
top-level describe block.

The following patterns are considered warnings:

```js
// Above a describe block
test('my test', () => {});
describe('test suite', () => {
it('test', () => {});
});

// Below a describe block
describe('test suite', () => {});
test('my test', () => {});

// Same for hooks
beforeAll('my beforeAll', () => {});
describe('test suite', () => {});
afterEach('my afterEach', () => {});
```

The following patterns are **not** considered warnings:

```js
// In a describe block
describe('test suite', () => {
test('my test', () => {});
});

// In a nested describe block
describe('test suite', () => {
test('my test', () => {});
describe('another test suite', () => {
test('my other test', () => {});
});
});
```

## When Not To Use It

Don't use this rule on non-jest test files.
2 changes: 1 addition & 1 deletion src/__tests__/rules.test.ts
Expand Up @@ -3,7 +3,7 @@ import { resolve } from 'path';
import plugin from '../';

const ruleNames = Object.keys(plugin.rules);
const numberOfRules = 38;
const numberOfRules = 39;

describe('rules', () => {
it('should have a corresponding doc for each rule', () => {
Expand Down
58 changes: 58 additions & 0 deletions src/rules/__tests__/require-top-level-describe.test.ts
@@ -0,0 +1,58 @@
import { TSESLint } from '@typescript-eslint/experimental-utils';
import rule from '../require-top-level-describe';

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

ruleTester.run('no-standalone-hook', rule, {
valid: [
'describe("test suite", () => { test("my test") });',
'describe("test suite", () => { it("my test") });',
`
describe("test suite", () => {
beforeEach("a", () => {});
describe("b", () => {});
test("c", () => {})
});
`,
'describe("test suite", () => { beforeAll("my beforeAll") });',
'describe("test suite", () => { afterEach("my afterEach") });',
'describe("test suite", () => { afterAll("my afterAll") });',
`
describe("test suite", () => {
it("my test", () => {})
describe("another test suite", () => {
});
test("my other test", () => {})
});
`,
],
invalid: [
{
code: `
test("my test", () => {})
describe("test suite", () => {});
`,
errors: [{ messageId: 'unexpectedTestCase' }],
},
{
code: `
test("my test", () => {})
describe("test suite", () => {
it("test", () => {})
});
`,
errors: [{ messageId: 'unexpectedTestCase' }],
},
{
code: `
describe("test suite", () => {});
afterAll("my test", () => {})
`,
errors: [{ messageId: 'unexpectedHook' }],
},
],
});
50 changes: 50 additions & 0 deletions src/rules/require-top-level-describe.ts
@@ -0,0 +1,50 @@
import { TSESTree } from '@typescript-eslint/experimental-utils';

import { createRule, isDescribe, isHook, isTestCase } from './utils';

export default createRule({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description:
'Prevents test cases and hooks to be outside of a describe block',
recommended: false,
},
messages: {
unexpectedTestCase: 'All test cases must be wrapped in a describe block.',
unexpectedHook: 'All hooks must be wrapped in a describe block.',
},
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
let numberOfDescribeBlocks = 0;
return {
CallExpression(node) {
if (isDescribe(node)) {
numberOfDescribeBlocks++;
return;
}

if (numberOfDescribeBlocks === 0) {
if (isTestCase(node)) {
context.report({ node, messageId: 'unexpectedTestCase' });
return;
}

if (isHook(node)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI complains that this is not covered. Maybe the else branch? A test with a CallExpression inside a describe that's neither a test nor a hook should probably hit it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed! I added a dummy test in the valid cases to fix it:

'foo()',

But thinking of it, should we trigger a warning if the user uses a CallExpression that's neither a test or a hook or can we leave it as is?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, that's fine, I think. Jest itself uses it to e.g. skip tests on windows, or run some other sort of helper 🙂

context.report({ node, messageId: 'unexpectedHook' });
return;
}
}
},
'CallExpression:exit'(node: TSESTree.CallExpression) {
if (isDescribe(node)) {
numberOfDescribeBlocks--;
}
},
};
},
});