Skip to content

Commit f09bc99

Browse files
cartogramSimenB
authored andcommitted
feat(rules): no-try-expect (#331)
1 parent 84df1d6 commit f09bc99

File tree

5 files changed

+174
-1
lines changed

5 files changed

+174
-1
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ installations requiring long-term consistency.
127127
| [no-test-prefixes][] | Disallow using `f` & `x` prefixes to define focused/skipped tests | ![recommended][] | ![fixable-green][] |
128128
| [no-test-return-statement][] | Disallow explicitly returning from tests | | |
129129
| [no-truthy-falsy][] | Disallow using `toBeTruthy()` & `toBeFalsy()` | | |
130+
| [no-try-expect][] | Prevent `catch` assertions in tests | | |
130131
| [prefer-expect-assertions][] | Suggest using `expect.assertions()` OR `expect.hasAssertions()` | | |
131132
| [prefer-spy-on][] | Suggest using `jest.spyOn()` | | ![fixable-green][] |
132133
| [prefer-strict-equal][] | Suggest using `toStrictEqual()` | | ![fixable-green][] |
@@ -177,6 +178,7 @@ https://github.com/dangreenisrael/eslint-plugin-jest-formatting
177178
[no-test-prefixes]: docs/rules/no-test-prefixes.md
178179
[no-test-return-statement]: docs/rules/no-test-return-statement.md
179180
[no-truthy-falsy]: docs/rules/no-truthy-falsy.md
181+
[no-try-expect]: docs/rules/no-try-expect.md
180182
[prefer-called-with]: docs/rules/prefer-called-with.md
181183
[prefer-expect-assertions]: docs/rules/prefer-expect-assertions.md
182184
[prefer-spy-on]: docs/rules/prefer-spy-on.md

docs/rules/no-try-expect.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Prevent catch assertions in tests (no-try-expect)
2+
3+
This rule prevents the use of `expect` inside `catch` blocks.
4+
5+
## Rule Details
6+
7+
Expectations inside a `catch` block can be silently skipped. While Jest provides
8+
an `expect.assertions(number)` helper, it might be cumbersome to add this to
9+
every single test. Using `toThrow` concisely guarantees that an exception was
10+
thrown, and that its contents match expectations.
11+
12+
The following patterns are warnings:
13+
14+
```js
15+
it('foo', () => {
16+
try {
17+
foo(); // `foo` may be refactored to not throw exceptions, yet still appears to be tested here.
18+
} catch (err) {
19+
expect(err).toMatch(/foo error/);
20+
}
21+
});
22+
23+
it('bar', async () => {
24+
try {
25+
await foo();
26+
} catch (err) {
27+
expect(err).toMatch(/foo error/);
28+
}
29+
});
30+
31+
it('baz', async () => {
32+
try {
33+
await foo();
34+
} catch (err) {
35+
expect(err).toMatchObject({ code: 'MODULE_NOT_FOUND' });
36+
}
37+
});
38+
```
39+
40+
The following patterns are not warnings:
41+
42+
```js
43+
it('foo', () => {
44+
expect(() => foo()).toThrow(/foo error/);
45+
});
46+
47+
it('bar', async () => {
48+
await expect(fooPromise).rejects.toThrow(/foo error/);
49+
});
50+
51+
it('baz', async () => {
52+
await expect(() => foo()).rejects.toThrow(
53+
expect.objectContaining({ code: 'MODULE_NOT_FOUND' }),
54+
);
55+
});
56+
```

src/__tests__/rules.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { resolve } from 'path';
33
import { rules } from '../';
44

55
const ruleNames = Object.keys(rules);
6-
const numberOfRules = 35;
6+
const numberOfRules = 36;
77

88
describe('rules', () => {
99
it('should have a corresponding doc for each rule', () => {
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { RuleTester } from 'eslint';
2+
import rule from '../no-try-expect';
3+
4+
const ruleTester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2019,
7+
},
8+
});
9+
10+
ruleTester.run('no-try-catch', rule, {
11+
valid: [
12+
`it('foo', () => {
13+
expect('foo').toEqual('foo');
14+
})`,
15+
`it('foo', () => {
16+
expect('bar').toEqual('bar');
17+
});
18+
try {
19+
20+
} catch {
21+
expect('foo').toEqual('foo');
22+
}`,
23+
`it.skip('foo');
24+
try {
25+
26+
} catch {
27+
expect('foo').toEqual('foo');
28+
}`,
29+
],
30+
invalid: [
31+
{
32+
code: `it('foo', () => {
33+
try {
34+
35+
} catch (err) {
36+
expect(err).toMatch('Error');
37+
}
38+
})`,
39+
errors: [
40+
{
41+
messageId: 'noTryExpect',
42+
},
43+
],
44+
},
45+
{
46+
code: `it('foo', async () => {
47+
await wrapper('production', async () => {
48+
try {
49+
50+
} catch (err) {
51+
expect(err).toMatch('Error');
52+
}
53+
})
54+
})`,
55+
errors: [
56+
{
57+
messageId: 'noTryExpect',
58+
},
59+
],
60+
},
61+
],
62+
});

src/rules/no-try-expect.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { getDocsUrl, isTestCase } from './util';
2+
3+
export default {
4+
meta: {
5+
docs: {
6+
description: 'Prefer using toThrow for exception tests',
7+
uri: getDocsUrl(__filename),
8+
},
9+
messages: {
10+
noTryExpect: [
11+
'Tests should use Jest‘s exception helpers.',
12+
'Use "expect(() => yourFunction()).toThrow()" for synchronous tests,',
13+
'or "await expect(yourFunction()).rejects.toThrow()" for async tests',
14+
].join(' '),
15+
},
16+
},
17+
create(context) {
18+
let isTest = false;
19+
let catchDepth = 0;
20+
21+
function isThrowExpectCall(node) {
22+
return catchDepth > 0 && node.callee.name === 'expect';
23+
}
24+
25+
return {
26+
CallExpression(node) {
27+
if (isTestCase(node)) {
28+
isTest = true;
29+
} else if (isTest && isThrowExpectCall(node)) {
30+
context.report({
31+
messageId: 'noTryExpect',
32+
node,
33+
});
34+
}
35+
},
36+
CatchClause() {
37+
if (isTest) {
38+
++catchDepth;
39+
}
40+
},
41+
'CatchClause:exit'() {
42+
if (isTest) {
43+
--catchDepth;
44+
}
45+
},
46+
'CallExpression:exit'(node) {
47+
if (isTestCase(node)) {
48+
isTest = false;
49+
}
50+
},
51+
};
52+
},
53+
};

0 commit comments

Comments
 (0)