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 no-restricted-jest-methods rule #1257

Merged
merged 1 commit into from Oct 3, 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 @@ -220,6 +220,7 @@ installations requiring long-term consistency.
| [no-jasmine-globals](docs/rules/no-jasmine-globals.md) | Disallow Jasmine globals | ![recommended][] | ![fixable][] |
| [no-large-snapshots](docs/rules/no-large-snapshots.md) | disallow large snapshots | | |
| [no-mocks-import](docs/rules/no-mocks-import.md) | Disallow manually importing from `__mocks__` | ![recommended][] | |
| [no-restricted-jest-methods](docs/rules/no-restricted-jest-methods.md) | Disallow specific `jest.` methods | | |
| [no-restricted-matchers](docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers | | |
| [no-standalone-expect](docs/rules/no-standalone-expect.md) | Disallow using `expect` outside of `it` or `test` blocks | ![recommended][] | |
| [no-test-prefixes](docs/rules/no-test-prefixes.md) | Use `.only` and `.skip` over `f` and `x` | ![recommended][] | ![fixable][] |
Expand Down
55 changes: 55 additions & 0 deletions docs/rules/no-restricted-jest-methods.md
@@ -0,0 +1,55 @@
# Disallow specific `jest.` methods (`no-restricted-jest-methods`)

💼 This rule is enabled in the following
[configs](https://github.com/jest-community/eslint-plugin-jest/blob/main/README.md#shareable-configurations):
`all`.

<!-- end rule header -->

You may wish to restrict the use of specific `jest` methods.

## Rule details

This rule checks for the usage of specific methods on the `jest` object, which
can be used to disallow curtain patterns such as spies and mocks.
Copy link
Contributor

Choose a reason for hiding this comment

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

just for my non native speaker understanding - are we really talking about curtain patterns here or is this a typo of certain patterns?

Copy link
Member

Choose a reason for hiding this comment

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

typo 😀

wanna send a PR fixing it?

Copy link
Contributor

Choose a reason for hiding this comment

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

More than happy to, coming right up

Copy link
Contributor

Choose a reason for hiding this comment

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


## Options

Restrictions are expressed in the form of a map, with the value being either a
string message to be shown, or `null` if a generic default message should be
used.

By default, this map is empty, meaning no `jest` methods are banned.

For example:

```json
{
"jest/no-restricted-jest-methods": [
"error",
{
"advanceTimersByTime": null,
"spyOn": "Don't use spies"
}
]
}
```

Examples of **incorrect** code for this rule with the above configuration

```js
jest.useFakeTimers();
it('calls the callback after 1 second via advanceTimersByTime', () => {
// ...

jest.advanceTimersByTime(1000);

// ...
});

test('plays video', () => {
const spy = jest.spyOn(video, 'play');

// ...
});
```
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/rules.test.ts.snap
Expand Up @@ -30,6 +30,7 @@ exports[`rules should export configs that refer to actual rules 1`] = `
"jest/no-jasmine-globals": "error",
"jest/no-large-snapshots": "error",
"jest/no-mocks-import": "error",
"jest/no-restricted-jest-methods": "error",
"jest/no-restricted-matchers": "error",
"jest/no-standalone-expect": "error",
"jest/no-test-prefixes": "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
110 changes: 110 additions & 0 deletions src/rules/__tests__/no-restricted-jest-methods.test.ts
@@ -0,0 +1,110 @@
import { TSESLint } from '@typescript-eslint/utils';
import dedent from 'dedent';
import rule from '../no-restricted-jest-methods';
import { espreeParser } from './test-utils';

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

ruleTester.run('no-restricted-jest-methods', rule, {
valid: [
'jest',
'jest.mock()',
'expect(a).rejects;',
'expect(a);',
{
code: dedent`
import { jest } from '@jest/globals';

jest;
`,
parserOptions: { sourceType: 'module' },
},
],
invalid: [
{
code: 'jest.fn()',
options: [{ fn: null }],
errors: [
{
messageId: 'restrictedJestMethod',
data: {
message: null,
restriction: 'fn',
},
column: 6,
line: 1,
},
],
},
{
code: 'jest["fn"]()',
options: [{ fn: null }],
errors: [
{
messageId: 'restrictedJestMethod',
data: {
message: null,
restriction: 'fn',
},
column: 6,
line: 1,
},
],
},
{
code: 'jest.mock()',
options: [{ mock: 'Do not use mocks' }],
errors: [
{
messageId: 'restrictedJestMethodWithMessage',
data: {
message: 'Do not use mocks',
restriction: 'mock',
},
column: 6,
line: 1,
},
],
},
{
code: 'jest["mock"]()',
options: [{ mock: 'Do not use mocks' }],
errors: [
{
messageId: 'restrictedJestMethodWithMessage',
data: {
message: 'Do not use mocks',
restriction: 'mock',
},
column: 6,
line: 1,
},
],
},
{
code: dedent`
import { jest } from '@jest/globals';

jest.advanceTimersByTime();
`,
options: [{ advanceTimersByTime: null }],
parserOptions: { sourceType: 'module' },
errors: [
{
messageId: 'restrictedJestMethod',
data: {
message: null,
restriction: 'advanceTimersByTime',
},
column: 6,
line: 3,
},
],
},
],
});
59 changes: 59 additions & 0 deletions src/rules/no-restricted-jest-methods.ts
@@ -0,0 +1,59 @@
import { createRule, getAccessorValue, parseJestFnCall } from './utils';

const messages = {
restrictedJestMethod: 'Use of `{{ restriction }}` is disallowed',
restrictedJestMethodWithMessage: '{{ message }}',
};

export default createRule<
[Record<string, string | null>],
keyof typeof messages
>({
name: __filename,
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow specific `jest.` methods',
recommended: false,
},
type: 'suggestion',
schema: [
{
type: 'object',
additionalProperties: {
type: ['string', 'null'],
},
},
],
messages,
},
defaultOptions: [{}],
create(context, [restrictedMethods]) {
return {
CallExpression(node) {
const jestFnCall = parseJestFnCall(node, context);

if (jestFnCall?.type !== 'jest') {
return;
}

const method = getAccessorValue(jestFnCall.members[0]);

if (method in restrictedMethods) {
const message = restrictedMethods[method];

context.report({
messageId: message
? 'restrictedJestMethodWithMessage'
: 'restrictedJestMethod',
data: { message, restriction: method },
loc: {
start: jestFnCall.members[0].loc.start,
end: jestFnCall.members[jestFnCall.members.length - 1].loc.end,
},
});
}
},
};
},
});