Skip to content

Commit

Permalink
feat: create no-restricted-jest-methods rule (#1257)
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath authored Oct 3, 2022
1 parent dbd072a commit b8e61b1
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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.

## 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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,
},
});
}
},
};
},
});

0 comments on commit b8e61b1

Please sign in to comment.