Skip to content

Commit 8d5fd33

Browse files
authoredJan 15, 2022
feat(prefer-expect-assertions): support requiring only if expect is used in a callback (#1028)
1 parent 618a8dd commit 8d5fd33

File tree

3 files changed

+545
-3
lines changed

3 files changed

+545
-3
lines changed
 

‎docs/rules/prefer-expect-assertions.md

+66
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,69 @@ describe('getNumbers', () => {
157157
});
158158
});
159159
```
160+
161+
#### `onlyFunctionsWithExpectInCallback`
162+
163+
When `true`, this rule will only warn for tests that have `expect` calls within
164+
a callback.
165+
166+
```json
167+
{
168+
"rules": {
169+
"jest/prefer-expect-assertions": [
170+
"warn",
171+
{ "onlyFunctionsWithExpectInCallback": true }
172+
]
173+
}
174+
}
175+
```
176+
177+
Examples of **incorrect** code when `'onlyFunctionsWithExpectInCallback'` is
178+
`true`:
179+
180+
```js
181+
describe('getNumbers', () => {
182+
it('only returns numbers that are greater than zero', () => {
183+
const numbers = getNumbers();
184+
185+
getNumbers().forEach(number => {
186+
expect(number).toBeGreaterThan(0);
187+
});
188+
});
189+
});
190+
191+
describe('/users', () => {
192+
it.each([1, 2, 3])('returns ok', id => {
193+
client.get(`/users/${id}`, response => {
194+
expect(response.status).toBe(200);
195+
});
196+
});
197+
});
198+
```
199+
200+
Examples of **correct** code when `'onlyFunctionsWithExpectInCallback'` is
201+
`true`:
202+
203+
```js
204+
describe('getNumbers', () => {
205+
it('only returns numbers that are greater than zero', () => {
206+
expect.hasAssertions();
207+
208+
const numbers = getNumbers();
209+
210+
getNumbers().forEach(number => {
211+
expect(number).toBeGreaterThan(0);
212+
});
213+
});
214+
});
215+
216+
describe('/users', () => {
217+
it.each([1, 2, 3])('returns ok', id => {
218+
expect.assertions(3);
219+
220+
client.get(`/users/${id}`, response => {
221+
expect(response.status).toBe(200);
222+
});
223+
});
224+
});
225+
```

‎src/rules/__tests__/prefer-expect-assertions.test.ts

+451
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,457 @@ ruleTester.run('prefer-expect-assertions (loops)', rule, {
718718
],
719719
});
720720

721+
ruleTester.run('prefer-expect-assertions (callbacks)', rule, {
722+
valid: [
723+
{
724+
code: dedent`
725+
const expectNumbersToBeGreaterThan = (numbers, value) => {
726+
numbers.forEach(number => {
727+
expect(number).toBeGreaterThan(value);
728+
});
729+
};
730+
731+
it('returns numbers that are greater than two', function () {
732+
expectNumbersToBeGreaterThan(getNumbers(), 2);
733+
});
734+
`,
735+
options: [{ onlyFunctionsWithExpectInCallback: true }],
736+
},
737+
{
738+
code: dedent`
739+
it('returns numbers that are greater than two', function () {
740+
expect.assertions(2);
741+
742+
const expectNumbersToBeGreaterThan = (numbers, value) => {
743+
for (let number of numbers) {
744+
expect(number).toBeGreaterThan(value);
745+
}
746+
};
747+
748+
expectNumbersToBeGreaterThan(getNumbers(), 2);
749+
});
750+
`,
751+
options: [{ onlyFunctionsWithExpectInCallback: true }],
752+
},
753+
{
754+
code: dedent`
755+
it("returns numbers that are greater than five", function () {
756+
expect.assertions(2);
757+
758+
getNumbers().forEach(number => {
759+
expect(number).toBeGreaterThan(5);
760+
});
761+
});
762+
`,
763+
options: [{ onlyFunctionsWithExpectInCallback: true }],
764+
},
765+
{
766+
code: dedent`
767+
it("returns things that are less than ten", function () {
768+
expect.hasAssertions();
769+
770+
things.forEach(thing => {
771+
expect(thing).toBeLessThan(10);
772+
});
773+
});
774+
`,
775+
options: [{ onlyFunctionsWithExpectInCallback: true }],
776+
},
777+
{
778+
code: dedent`
779+
it('sends the data as a string', () => {
780+
expect.hasAssertions();
781+
782+
const stream = openStream();
783+
784+
stream.on('data', data => {
785+
expect(data).toBe(expect.any(String));
786+
});
787+
});
788+
`,
789+
options: [{ onlyFunctionsWithExpectInCallback: true }],
790+
},
791+
{
792+
code: dedent`
793+
it('responds ok', function () {
794+
expect.assertions(1);
795+
796+
client.get('/user', response => {
797+
expect(response.status).toBe(200);
798+
});
799+
});
800+
`,
801+
options: [{ onlyFunctionsWithExpectInCallback: true }],
802+
},
803+
{
804+
code: dedent`
805+
it.each([1, 2, 3])("returns ok", id => {
806+
expect.assertions(3);
807+
808+
client.get(\`/users/$\{id}\`, response => {
809+
expect(response.status).toBe(200);
810+
});
811+
});
812+
813+
it("is a number that is greater than four", () => {
814+
expect(number).toBeGreaterThan(4);
815+
});
816+
`,
817+
options: [{ onlyFunctionsWithExpectInCallback: true }],
818+
},
819+
],
820+
invalid: [
821+
{
822+
code: dedent`
823+
it('sends the data as a string', () => {
824+
const stream = openStream();
825+
826+
stream.on('data', data => {
827+
expect(data).toBe(expect.any(String));
828+
});
829+
});
830+
`,
831+
options: [{ onlyFunctionsWithExpectInCallback: true }],
832+
errors: [
833+
{
834+
messageId: 'haveExpectAssertions',
835+
column: 1,
836+
line: 1,
837+
},
838+
],
839+
},
840+
{
841+
code: dedent`
842+
it('responds ok', function () {
843+
client.get('/user', response => {
844+
expect(response.status).toBe(200);
845+
});
846+
});
847+
`,
848+
options: [{ onlyFunctionsWithExpectInCallback: true }],
849+
errors: [
850+
{
851+
messageId: 'haveExpectAssertions',
852+
column: 1,
853+
line: 1,
854+
},
855+
],
856+
},
857+
{
858+
code: dedent`
859+
it('responds ok', function () {
860+
client.get('/user', response => {
861+
expect.assertions(1);
862+
863+
expect(response.status).toBe(200);
864+
});
865+
});
866+
`,
867+
options: [{ onlyFunctionsWithExpectInCallback: true }],
868+
errors: [
869+
{
870+
messageId: 'haveExpectAssertions',
871+
column: 1,
872+
line: 1,
873+
},
874+
],
875+
},
876+
{
877+
code: dedent`
878+
it('responds ok', function () {
879+
const expectOkResponse = response => {
880+
expect.assertions(1);
881+
882+
expect(response.status).toBe(200);
883+
};
884+
885+
client.get('/user', expectOkResponse);
886+
});
887+
`,
888+
options: [{ onlyFunctionsWithExpectInCallback: true }],
889+
errors: [
890+
{
891+
messageId: 'haveExpectAssertions',
892+
column: 1,
893+
line: 1,
894+
},
895+
],
896+
},
897+
{
898+
code: dedent`
899+
it('returns numbers that are greater than two', function () {
900+
const expectNumberToBeGreaterThan = (number, value) => {
901+
expect(number).toBeGreaterThan(value);
902+
};
903+
904+
expectNumberToBeGreaterThan(1, 2);
905+
});
906+
`,
907+
options: [{ onlyFunctionsWithExpectInCallback: true }],
908+
errors: [
909+
{
910+
messageId: 'haveExpectAssertions',
911+
column: 1,
912+
line: 1,
913+
},
914+
],
915+
},
916+
{
917+
code: dedent`
918+
it('returns numbers that are greater than two', function () {
919+
const expectNumbersToBeGreaterThan = (numbers, value) => {
920+
for (let number of numbers) {
921+
expect(number).toBeGreaterThan(value);
922+
}
923+
};
924+
925+
expectNumbersToBeGreaterThan(getNumbers(), 2);
926+
});
927+
`,
928+
options: [{ onlyFunctionsWithExpectInCallback: true }],
929+
errors: [
930+
{
931+
messageId: 'haveExpectAssertions',
932+
column: 1,
933+
line: 1,
934+
},
935+
],
936+
},
937+
{
938+
code: dedent`
939+
it('only returns numbers that are greater than six', () => {
940+
getNumbers().forEach(number => {
941+
expect(number).toBeGreaterThan(6);
942+
});
943+
});
944+
`,
945+
options: [{ onlyFunctionsWithExpectInCallback: true }],
946+
errors: [
947+
{
948+
messageId: 'haveExpectAssertions',
949+
column: 1,
950+
line: 1,
951+
},
952+
],
953+
},
954+
{
955+
code: dedent`
956+
it("is wrong");
957+
958+
it('responds ok', function () {
959+
const expectOkResponse = response => {
960+
expect.assertions(1);
961+
962+
expect(response.status).toBe(200);
963+
};
964+
965+
client.get('/user', expectOkResponse);
966+
});
967+
`,
968+
options: [{ onlyFunctionsWithExpectInCallback: true }],
969+
errors: [
970+
{
971+
messageId: 'haveExpectAssertions',
972+
column: 1,
973+
line: 3,
974+
},
975+
],
976+
},
977+
{
978+
code: dedent`
979+
it("is a number that is greater than four", () => {
980+
expect(number).toBeGreaterThan(4);
981+
});
982+
983+
it('responds ok', function () {
984+
const expectOkResponse = response => {
985+
expect(response.status).toBe(200);
986+
};
987+
988+
client.get('/user', expectOkResponse);
989+
});
990+
991+
it("returns numbers that are greater than five", () => {
992+
expect(number).toBeGreaterThan(5);
993+
});
994+
`,
995+
options: [{ onlyFunctionsWithExpectInCallback: true }],
996+
errors: [
997+
{
998+
messageId: 'haveExpectAssertions',
999+
column: 1,
1000+
line: 5,
1001+
},
1002+
],
1003+
},
1004+
{
1005+
code: dedent`
1006+
it("is a number that is greater than four", () => {
1007+
expect(number).toBeGreaterThan(4);
1008+
});
1009+
1010+
it("returns numbers that are greater than four", () => {
1011+
getNumbers().map(number => {
1012+
expect(number).toBeGreaterThan(0);
1013+
});
1014+
});
1015+
1016+
it("returns numbers that are greater than five", () => {
1017+
expect(number).toBeGreaterThan(5);
1018+
});
1019+
`,
1020+
options: [{ onlyFunctionsWithExpectInCallback: true }],
1021+
errors: [
1022+
{
1023+
messageId: 'haveExpectAssertions',
1024+
column: 1,
1025+
line: 5,
1026+
},
1027+
],
1028+
},
1029+
{
1030+
code: dedent`
1031+
it.each([1, 2, 3])("returns ok", id => {
1032+
client.get(\`/users/$\{id}\`, response => {
1033+
expect(response.status).toBe(200);
1034+
});
1035+
});
1036+
1037+
it("is a number that is greater than four", () => {
1038+
expect(number).toBeGreaterThan(4);
1039+
});
1040+
`,
1041+
options: [{ onlyFunctionsWithExpectInCallback: true }],
1042+
errors: [
1043+
{
1044+
messageId: 'haveExpectAssertions',
1045+
column: 1,
1046+
line: 1,
1047+
},
1048+
],
1049+
},
1050+
{
1051+
code: dedent`
1052+
it('responds ok', function () {
1053+
client.get('/user', response => {
1054+
expect(response.status).toBe(200);
1055+
});
1056+
});
1057+
1058+
it("is a number that is greater than four", () => {
1059+
expect(number).toBeGreaterThan(4);
1060+
});
1061+
`,
1062+
options: [{ onlyFunctionsWithExpectInCallback: true }],
1063+
errors: [
1064+
{
1065+
messageId: 'haveExpectAssertions',
1066+
column: 1,
1067+
line: 1,
1068+
},
1069+
],
1070+
},
1071+
{
1072+
code: dedent`
1073+
it('responds ok', function () {
1074+
client.get('/user', response => {
1075+
expect(response.status).toBe(200);
1076+
});
1077+
});
1078+
1079+
it("is a number that is greater than four", () => {
1080+
expect.hasAssertions();
1081+
1082+
expect(number).toBeGreaterThan(4);
1083+
});
1084+
`,
1085+
options: [{ onlyFunctionsWithExpectInCallback: true }],
1086+
errors: [
1087+
{
1088+
messageId: 'haveExpectAssertions',
1089+
column: 1,
1090+
line: 1,
1091+
},
1092+
],
1093+
},
1094+
{
1095+
code: dedent`
1096+
it("it1", () => {
1097+
expect.hasAssertions();
1098+
1099+
getNumbers().forEach(number => {
1100+
expect(number).toBeGreaterThan(0);
1101+
});
1102+
});
1103+
1104+
it("it1", () => {
1105+
getNumbers().forEach(number => {
1106+
expect(number).toBeGreaterThan(0);
1107+
});
1108+
});
1109+
`,
1110+
options: [{ onlyFunctionsWithExpectInCallback: true }],
1111+
errors: [
1112+
{
1113+
messageId: 'haveExpectAssertions',
1114+
column: 1,
1115+
line: 9,
1116+
},
1117+
],
1118+
},
1119+
{
1120+
code: dedent`
1121+
it('responds ok', function () {
1122+
expect.hasAssertions();
1123+
1124+
client.get('/user', response => {
1125+
expect(response.status).toBe(200);
1126+
});
1127+
});
1128+
1129+
it('responds not found', function () {
1130+
client.get('/user', response => {
1131+
expect(response.status).toBe(404);
1132+
});
1133+
});
1134+
`,
1135+
options: [{ onlyFunctionsWithExpectInCallback: true }],
1136+
errors: [
1137+
{
1138+
messageId: 'haveExpectAssertions',
1139+
column: 1,
1140+
line: 9,
1141+
},
1142+
],
1143+
},
1144+
{
1145+
code: dedent`
1146+
it.skip.each\`\`("it1", async () => {
1147+
expect.hasAssertions();
1148+
1149+
client.get('/user', response => {
1150+
expect(response.status).toBe(200);
1151+
});
1152+
});
1153+
1154+
it("responds ok", () => {
1155+
client.get('/user', response => {
1156+
expect(response.status).toBe(200);
1157+
});
1158+
});
1159+
`,
1160+
options: [{ onlyFunctionsWithExpectInCallback: true }],
1161+
errors: [
1162+
{
1163+
messageId: 'haveExpectAssertions',
1164+
column: 1,
1165+
line: 9,
1166+
},
1167+
],
1168+
},
1169+
],
1170+
});
1171+
7211172
ruleTester.run('prefer-expect-assertions (mixed)', rule, {
7221173
valid: [
7231174
{

‎src/rules/prefer-expect-assertions.ts

+28-3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const suggestRemovingExtraArguments = (
4646
interface RuleOptions {
4747
onlyFunctionsWithAsyncKeyword?: boolean;
4848
onlyFunctionsWithExpectInLoop?: boolean;
49+
onlyFunctionsWithExpectInCallback?: boolean;
4950
}
5051

5152
type MessageIds =
@@ -93,6 +94,7 @@ export default createRule<[RuleOptions], MessageIds>({
9394
properties: {
9495
onlyFunctionsWithAsyncKeyword: { type: 'boolean' },
9596
onlyFunctionsWithExpectInLoop: { type: 'boolean' },
97+
onlyFunctionsWithExpectInCallback: { type: 'boolean' },
9698
},
9799
additionalProperties: false,
98100
},
@@ -102,17 +104,21 @@ export default createRule<[RuleOptions], MessageIds>({
102104
{
103105
onlyFunctionsWithAsyncKeyword: false,
104106
onlyFunctionsWithExpectInLoop: false,
107+
onlyFunctionsWithExpectInCallback: false,
105108
},
106109
],
107110
create(context, [options]) {
111+
let expressionDepth = 0;
112+
let hasExpectInCallback = false;
108113
let hasExpectInLoop = false;
109114
let inTestCaseCall = false;
110115
let inForLoop = false;
111116

112117
const shouldCheckFunction = (testFunction: TSESTree.FunctionLike) => {
113118
if (
114119
!options.onlyFunctionsWithAsyncKeyword &&
115-
!options.onlyFunctionsWithExpectInLoop
120+
!options.onlyFunctionsWithExpectInLoop &&
121+
!options.onlyFunctionsWithExpectInCallback
116122
) {
117123
return true;
118124
}
@@ -129,13 +135,25 @@ export default createRule<[RuleOptions], MessageIds>({
129135
}
130136
}
131137

138+
if (options.onlyFunctionsWithExpectInCallback) {
139+
if (hasExpectInCallback) {
140+
return true;
141+
}
142+
}
143+
132144
return false;
133145
};
134146

147+
const enterExpression = () => inTestCaseCall && expressionDepth++;
148+
const exitExpression = () => inTestCaseCall && expressionDepth--;
135149
const enterForLoop = () => (inForLoop = true);
136150
const exitForLoop = () => (inForLoop = false);
137151

138152
return {
153+
FunctionExpression: enterExpression,
154+
'FunctionExpression:exit': exitExpression,
155+
ArrowFunctionExpression: enterExpression,
156+
'ArrowFunctionExpression:exit': exitExpression,
139157
ForStatement: enterForLoop,
140158
'ForStatement:exit': exitForLoop,
141159
ForInStatement: enterForLoop,
@@ -149,8 +167,14 @@ export default createRule<[RuleOptions], MessageIds>({
149167
return;
150168
}
151169

152-
if (isExpectCall(node) && inTestCaseCall && inForLoop) {
153-
hasExpectInLoop = true;
170+
if (isExpectCall(node) && inTestCaseCall) {
171+
if (inForLoop) {
172+
hasExpectInLoop = true;
173+
}
174+
175+
if (expressionDepth > 1) {
176+
hasExpectInCallback = true;
177+
}
154178
}
155179
},
156180
'CallExpression:exit'(node: TSESTree.CallExpression) {
@@ -176,6 +200,7 @@ export default createRule<[RuleOptions], MessageIds>({
176200
}
177201

178202
hasExpectInLoop = false;
203+
hasExpectInCallback = false;
179204

180205
const testFuncBody = testFn.body.body;
181206

0 commit comments

Comments
 (0)
Please sign in to comment.