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-to-be rule #864

Merged
merged 10 commits into from
Sep 29, 2021
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ installations requiring long-term consistency.
| [prefer-hooks-on-top](docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | |
| [prefer-spy-on](docs/rules/prefer-spy-on.md) | Suggest using `jest.spyOn()` | | ![fixable][] |
| [prefer-strict-equal](docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | ![suggest][] |
| [prefer-to-be](docs/rules/prefer-to-be.md) | Suggest using `toBe()` for primitive literals | | ![fixable][] |
| [prefer-to-be-null](docs/rules/prefer-to-be-null.md) | Suggest using `toBeNull()` | ![style][] | ![fixable][] |
| [prefer-to-be-undefined](docs/rules/prefer-to-be-undefined.md) | Suggest using `toBeUndefined()` | ![style][] | ![fixable][] |
| [prefer-to-contain](docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | ![style][] | ![fixable][] |
Expand Down
53 changes: 53 additions & 0 deletions docs/rules/prefer-to-be.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Suggest using `toBe()` for primitive literals (`prefer-to-be`)

When asserting against primitive literals such as numbers and strings, the
equality matchers all operate the same, but read slightly differently in code.

This rule recommends using the `toBe` matcher in these situations, as it forms
the most grammatically natural sentence. For `null`, `undefined`, and `NaN` this
rule recommends using their specific `toBe` matchers, as they give better error
messages as well.

## Rule details

This rule triggers a warning if `toEqual()` or `toStrictEqual()` are used to
assert a primitive literal value such as numbers, strings, and booleans.

The following patterns are considered warnings:

```js
expect(value).not.toEqual(5);
expect(getMessage()).toStrictEqual('hello world');
expect(loadMessage()).resolves.toEqual('hello world');
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
```

The following pattern is not warning:

```js
expect(value).not.toBe(5);
expect(getMessage()).toBe('hello world');
expect(loadMessage()).resolves.toBe('hello world');
expect(didError).not.toBe(true);

expect(catchError()).toStrictEqual({ message: 'oh noes!' });
```

For `null`, `undefined`, and `NaN`, this rule triggers a warning if `toBe` is
used to assert against those literal values instead of their more specific
`toBe` counterparts:

```js
expect(value).not.toBe(undefined);
expect(getMessage()).toBe(null);
expect(countMessages()).resolves.not.toBe(NaN);
```

The following pattern is not warning:

```js
expect(value).toBeDefined();
expect(getMessage()).toBeNull();
expect(countMessages()).resolves.not.toBeNaN();

expect(catchError()).toStrictEqual({ message: undefined });
```
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 @@ -40,6 +40,7 @@ Object {
"jest/prefer-hooks-on-top": "error",
"jest/prefer-spy-on": "error",
"jest/prefer-strict-equal": "error",
"jest/prefer-to-be": "error",
"jest/prefer-to-be-null": "error",
"jest/prefer-to-be-undefined": "error",
"jest/prefer-to-contain": "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 = 46;
const numberOfRules = 47;
const ruleNames = Object.keys(plugin.rules);
const deprecatedRules = Object.entries(plugin.rules)
.filter(([, rule]) => rule.meta.deprecated)
Expand Down
270 changes: 270 additions & 0 deletions src/rules/__tests__/prefer-to-be.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { TSESLint } from '@typescript-eslint/experimental-utils';
import rule from '../prefer-to-be';

const ruleTester = new TSESLint.RuleTester();

ruleTester.run('prefer-to-be', rule, {
valid: [
'expect(null).toBeNull();',
'expect(null).not.toBeNull();',
'expect(null).toBe(1);',
'expect(obj).toStrictEqual([ x, 1 ]);',
'expect(obj).toStrictEqual({ x: 1 });',
'expect(obj).not.toStrictEqual({ x: 1 });',
'expect(value).toMatchSnapshot();',
"expect(catchError()).toStrictEqual({ message: 'oh noes!' })",
'expect("something");',
],
invalid: [
{
code: 'expect(value).toEqual("my string");',
output: 'expect(value).toBe("my string");',
errors: [{ messageId: 'useToBe', column: 15, line: 1 }],
},
{
code: 'expect(value).toStrictEqual("my string");',
output: 'expect(value).toBe("my string");',
errors: [{ messageId: 'useToBe', column: 15, line: 1 }],
},
{
code: 'expect(value).toStrictEqual(1);',
output: 'expect(value).toBe(1);',
errors: [{ messageId: 'useToBe', column: 15, line: 1 }],
},
{
code: 'expect(loadMessage()).resolves.toStrictEqual("hello world");',
output: 'expect(loadMessage()).resolves.toBe("hello world");',
errors: [{ messageId: 'useToBe', column: 32, line: 1 }],
},
{
code: 'expect(loadMessage()).resolves.toStrictEqual(false);',
output: 'expect(loadMessage()).resolves.toBe(false);',
errors: [{ messageId: 'useToBe', column: 32, line: 1 }],
},
],
});

ruleTester.run('prefer-to-be: null', rule, {
valid: [
'expect(null).toBeNull();',
'expect(null).not.toBeNull();',
'expect(null).toBe(1);',
'expect(obj).toStrictEqual([ x, 1 ]);',
'expect(obj).toStrictEqual({ x: 1 });',
'expect(obj).not.toStrictEqual({ x: 1 });',
'expect(value).toMatchSnapshot();',
"expect(catchError()).toStrictEqual({ message: 'oh noes!' })",
'expect("something");',
//
'expect(null).not.toEqual();',
'expect(null).toBe();',
'expect(null).toMatchSnapshot();',
'expect("a string").toMatchSnapshot(null);',
'expect("a string").not.toMatchSnapshot();',
'expect(null).toBe',
],
invalid: [
{
code: 'expect(null).toBe(null);',
output: 'expect(null).toBeNull();',
errors: [{ messageId: 'useToBeNull', column: 14, line: 1 }],
},
{
code: 'expect(null).toEqual(null);',
output: 'expect(null).toBeNull();',
errors: [{ messageId: 'useToBeNull', column: 14, line: 1 }],
},
{
code: 'expect(null).toStrictEqual(null);',
output: 'expect(null).toBeNull();',
errors: [{ messageId: 'useToBeNull', column: 14, line: 1 }],
},
{
code: 'expect("a string").not.toBe(null);',
output: 'expect("a string").not.toBeNull();',
errors: [{ messageId: 'useToBeNull', column: 24, line: 1 }],
},
{
code: 'expect("a string").not.toEqual(null);',
output: 'expect("a string").not.toBeNull();',
errors: [{ messageId: 'useToBeNull', column: 24, line: 1 }],
},
{
code: 'expect("a string").not.toStrictEqual(null);',
output: 'expect("a string").not.toBeNull();',
errors: [{ messageId: 'useToBeNull', column: 24, line: 1 }],
},
],
});

ruleTester.run('prefer-to-be: undefined', rule, {
valid: [
'expect(undefined).toBeUndefined();',
'expect(true).toBeDefined();',
'expect({}).toEqual({});',
'expect(something).toBe()',
'expect(something).toBe(somethingElse)',
'expect(something).toEqual(somethingElse)',
'expect(something).not.toBe(somethingElse)',
'expect(something).not.toEqual(somethingElse)',
'expect(undefined).toBe',
'expect("something");',
],

invalid: [
{
code: 'expect(undefined).toBe(undefined);',
output: 'expect(undefined).toBeUndefined();',
errors: [{ messageId: 'useToBeUndefined', column: 19, line: 1 }],
},
{
code: 'expect(undefined).toEqual(undefined);',
output: 'expect(undefined).toBeUndefined();',
errors: [{ messageId: 'useToBeUndefined', column: 19, line: 1 }],
},
{
code: 'expect(undefined).toStrictEqual(undefined);',
output: 'expect(undefined).toBeUndefined();',
errors: [{ messageId: 'useToBeUndefined', column: 19, line: 1 }],
},
{
code: 'expect("a string").not.toBe(undefined);',
output: 'expect("a string").toBeDefined();',
errors: [{ messageId: 'useToBeDefined', column: 24, line: 1 }],
},
{
code: 'expect("a string").not.toEqual(undefined);',
output: 'expect("a string").toBeDefined();',
errors: [{ messageId: 'useToBeDefined', column: 24, line: 1 }],
},
{
code: 'expect("a string").not.toStrictEqual(undefined);',
output: 'expect("a string").toBeDefined();',
errors: [{ messageId: 'useToBeDefined', column: 24, line: 1 }],
},
],
});

ruleTester.run('prefer-to-be: NaN', rule, {
valid: [
'expect(NaN).toBeNaN();',
'expect(true).not.toBeNaN();',
'expect({}).toEqual({});',
'expect(something).toBe()',
'expect(something).toBe(somethingElse)',
'expect(something).toEqual(somethingElse)',
'expect(something).not.toBe(somethingElse)',
'expect(something).not.toEqual(somethingElse)',
'expect(undefined).toBe',
'expect("something");',
],
invalid: [
{
code: 'expect(NaN).toBe(NaN);',
output: 'expect(NaN).toBeNaN();',
errors: [{ messageId: 'useToBeNaN', column: 13, line: 1 }],
},
{
code: 'expect(NaN).toEqual(NaN);',
output: 'expect(NaN).toBeNaN();',
errors: [{ messageId: 'useToBeNaN', column: 13, line: 1 }],
},
{
code: 'expect(NaN).toStrictEqual(NaN);',
output: 'expect(NaN).toBeNaN();',
errors: [{ messageId: 'useToBeNaN', column: 13, line: 1 }],
},
{
code: 'expect("a string").not.toBe(NaN);',
output: 'expect("a string").not.toBeNaN();',
errors: [{ messageId: 'useToBeNaN', column: 24, line: 1 }],
},
{
code: 'expect("a string").not.toEqual(NaN);',
output: 'expect("a string").not.toBeNaN();',
errors: [{ messageId: 'useToBeNaN', column: 24, line: 1 }],
},
{
code: 'expect("a string").not.toStrictEqual(NaN);',
output: 'expect("a string").not.toBeNaN();',
errors: [{ messageId: 'useToBeNaN', column: 24, line: 1 }],
},
],
});

ruleTester.run('prefer-to-be: undefined vs defined', rule, {
valid: [
'expect(NaN).toBeNaN();',
'expect(true).not.toBeNaN();',
'expect({}).toEqual({});',
'expect(something).toBe()',
'expect(something).toBe(somethingElse)',
'expect(something).toEqual(somethingElse)',
'expect(something).not.toBe(somethingElse)',
'expect(something).not.toEqual(somethingElse)',
'expect(undefined).toBe',
'expect("something");',
],
invalid: [
{
code: 'expect(undefined).not.toBeDefined();',
output: 'expect(undefined).toBeUndefined();',
errors: [{ messageId: 'useToBeUndefined', column: 23, line: 1 }],
},
{
code: 'expect(undefined).resolves.not.toBeDefined();',
output: 'expect(undefined).resolves.toBeUndefined();',
errors: [{ messageId: 'useToBeUndefined', column: 32, line: 1 }],
},
{
code: 'expect("a string").not.toBeUndefined();',
output: 'expect("a string").toBeDefined();',
errors: [{ messageId: 'useToBeDefined', column: 24, line: 1 }],
},
{
code: 'expect("a string").rejects.not.toBeUndefined();',
output: 'expect("a string").rejects.toBeDefined();',
errors: [{ messageId: 'useToBeDefined', column: 32, line: 1 }],
},
],
});

new TSESLint.RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
}).run('prefer-to-be: typescript edition', rule, {
valid: [
"(expect('Model must be bound to an array if the multiple property is true') as any).toHaveBeenTipped()",
],
invalid: [
{
code: 'expect(null).toEqual(1 as unknown as string as unknown as any);',
output: 'expect(null).toBe(1 as unknown as string as unknown as any);',
errors: [{ messageId: 'useToBe', column: 14, line: 1 }],
},
{
code: 'expect("a string").not.toStrictEqual("string" as number);',
output: 'expect("a string").not.toBe("string" as number);',
errors: [{ messageId: 'useToBe', column: 24, line: 1 }],
},
{
code: 'expect(null).toBe(null as unknown as string as unknown as any);',
output: 'expect(null).toBeNull();',
errors: [{ messageId: 'useToBeNull', column: 14, line: 1 }],
},
{
code: 'expect("a string").not.toEqual(null as number);',
output: 'expect("a string").not.toBeNull();',
errors: [{ messageId: 'useToBeNull', column: 24, line: 1 }],
},
{
code: 'expect(undefined).toBe(undefined as unknown as string as any);',
output: 'expect(undefined).toBeUndefined();',
errors: [{ messageId: 'useToBeUndefined', column: 19, line: 1 }],
},
{
code: 'expect("a string").toEqual(undefined as number);',
output: 'expect("a string").toBeUndefined();',
errors: [{ messageId: 'useToBeUndefined', column: 20, line: 1 }],
},
],
});
Loading