Skip to content

Commit

Permalink
feat: create prefer-to-be rule (#864)
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath authored Sep 29, 2021
1 parent 6940488 commit 3a64aea
Show file tree
Hide file tree
Showing 7 changed files with 499 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,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');
```

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

0 comments on commit 3a64aea

Please sign in to comment.