Skip to content

Commit 66cafd9

Browse files
authored
fix(expect,testing,internal): throw if expect.hasAssertion and expect.assertions are not checked (#6646)
1 parent 04ba040 commit 66cafd9

File tree

5 files changed

+244
-78
lines changed

5 files changed

+244
-78
lines changed

expect/_assertions_test.ts

Lines changed: 160 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,180 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

3-
import { describe, it, test } from "@std/testing/bdd";
4-
import { expect } from "./expect.ts";
5-
6-
Deno.test("expect.hasAssertions() API", () => {
7-
describe("describe suite", () => {
8-
// FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot`
9-
it("should throw an error", () => {
10-
expect.hasAssertions();
11-
});
3+
import * as path from "@std/path";
4+
import { assertStringIncludes } from "@std/assert";
5+
import { stripAnsiCode } from "@std/internal/styles";
126

13-
it("should pass", () => {
14-
expect.hasAssertions();
15-
expect("a").toEqual("a");
16-
});
17-
});
18-
19-
it("it() suite should pass", () => {
20-
expect.hasAssertions();
21-
expect("a").toEqual("a");
7+
Deno.test("expect.hasAssertions() API", async () => {
8+
const tempDirPath = await Deno.makeTempDir({
9+
prefix: "deno_std_has_assertions_",
2210
});
11+
try {
12+
const tempFilePath = path.join(tempDirPath, "has_assertions_test.ts");
13+
await Deno.writeTextFile(
14+
tempFilePath,
15+
`import { describe, it, test } from "@std/testing/bdd";
16+
import { expect } from "@std/expect";
2317
24-
// FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot`
25-
test("test suite should throw an error", () => {
18+
describe("describe suite", () => {
19+
it("describe should throw an error", () => {
2620
expect.hasAssertions();
2721
});
2822
29-
test("test suite should pass", () => {
23+
it("describe should pass", () => {
3024
expect.hasAssertions();
3125
expect("a").toEqual("a");
3226
});
3327
});
3428
35-
Deno.test("expect.assertions() API", () => {
36-
test("should pass", () => {
37-
expect.assertions(2);
38-
expect("a").not.toBe("b");
39-
expect("a").toBe("a");
40-
});
29+
it("it() suite should throw an error", () => {
30+
expect.hasAssertions();
31+
});
4132
42-
// FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot`
43-
test("should throw error", () => {
44-
expect.assertions(1);
45-
expect("a").not.toBe("b");
46-
expect("a").toBe("a");
47-
});
33+
it("it() suite should pass", () => {
34+
expect.hasAssertions();
35+
expect("a").toEqual("a");
36+
});
4837
49-
it("redeclare different assertion count", () => {
50-
expect.assertions(3);
51-
expect("a").not.toBe("b");
52-
expect("a").toBe("a");
53-
expect.assertions(2);
54-
});
38+
test("test suite should throw an error", () => {
39+
expect.hasAssertions();
40+
});
41+
42+
test("test suite should pass", () => {
43+
expect.hasAssertions();
44+
expect("a").toEqual("a");
45+
});
46+
`,
47+
);
48+
49+
const command = new Deno.Command(Deno.execPath(), {
50+
args: ["test", "--no-lock", tempDirPath],
51+
});
52+
const { stdout } = await command.output();
53+
const errorMessage = stripAnsiCode(new TextDecoder().decode(stdout));
54+
55+
assertStringIncludes(
56+
errorMessage,
57+
"describe should throw an error ... FAILED",
58+
);
59+
assertStringIncludes(errorMessage, "describe should pass ... ok");
60+
assertStringIncludes(
61+
errorMessage,
62+
"it() suite should throw an error ... FAILED",
63+
);
64+
assertStringIncludes(errorMessage, "it() suite should pass ... ok");
65+
assertStringIncludes(
66+
errorMessage,
67+
"test suite should throw an error ... FAILED",
68+
);
69+
assertStringIncludes(errorMessage, "test suite should pass ... ok");
5570

56-
test("expect no assertions", () => {
57-
expect.assertions(0);
71+
assertStringIncludes(
72+
errorMessage,
73+
"error: AssertionError: Expected at least one assertion to be called but received none",
74+
);
75+
} finally {
76+
await Deno.remove(tempDirPath, { recursive: true });
77+
}
78+
});
79+
80+
Deno.test("expect.assertions() API", async () => {
81+
const tempDirPath = await Deno.makeTempDir({
82+
prefix: "deno_std_has_assertions_",
5883
});
84+
try {
85+
const tempFilePath = path.join(tempDirPath, "has_assertions_test.ts");
86+
await Deno.writeTextFile(
87+
tempFilePath,
88+
`import { describe, it, test } from "@std/testing/bdd";
89+
import { expect } from "@std/expect";
5990
60-
// FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot`
61-
it("should throw an error", () => {
62-
expect.assertions(2);
91+
test("should pass", () => {
92+
expect.assertions(2);
93+
expect("a").not.toBe("b");
94+
expect("a").toBe("a");
95+
});
96+
97+
test("should throw error", () => {
98+
expect.assertions(1);
99+
expect("a").not.toBe("b");
100+
expect("a").toBe("a");
101+
});
102+
103+
it("redeclare different assertion count", () => {
104+
expect.assertions(3);
105+
expect("a").not.toBe("b");
106+
expect("a").toBe("a");
107+
expect.assertions(2);
108+
});
109+
110+
test("expect no assertions", () => {
111+
expect.assertions(0);
112+
});
113+
114+
it("should throw an error", () => {
115+
expect.assertions(2);
116+
});
117+
`,
118+
);
119+
120+
const command = new Deno.Command(Deno.execPath(), {
121+
args: ["test", "--no-lock", tempDirPath],
122+
});
123+
const { stdout } = await command.output();
124+
const errorMessage = stripAnsiCode(new TextDecoder().decode(stdout));
125+
126+
assertStringIncludes(errorMessage, "should pass ... ok");
127+
assertStringIncludes(errorMessage, "should throw error ... FAILED");
128+
assertStringIncludes(
129+
errorMessage,
130+
"redeclare different assertion count ... ok",
131+
);
132+
assertStringIncludes(errorMessage, "expect no assertions ... ok");
133+
assertStringIncludes(errorMessage, "should throw an error ... FAILED");
134+
135+
assertStringIncludes(
136+
errorMessage,
137+
"error: AssertionError: Expected exactly 1 assertion to be called, but received 2",
138+
);
139+
assertStringIncludes(
140+
errorMessage,
141+
"error: AssertionError: Expected exactly 2 assertions to be called, but received 0",
142+
);
143+
} finally {
144+
await Deno.remove(tempDirPath, { recursive: true });
145+
}
146+
});
147+
148+
Deno.test("expect assertions reset after errored tests", async () => {
149+
const tempDirPath = await Deno.makeTempDir({
150+
prefix: "deno_std_assertions_reset_",
63151
});
152+
try {
153+
const tempFilePath = path.join(tempDirPath, "assertion_reset_test.ts");
154+
await Deno.writeTextFile(
155+
tempFilePath,
156+
`import { it } from "@std/testing/bdd";
157+
import { expect } from "@std/expect";
158+
it("should fail", () => {
159+
expect.assertions(2);
160+
expect(1).toBe(1);
161+
});
162+
it("should pass", () => {
163+
expect.assertions(0);
164+
});
165+
`,
166+
);
167+
168+
const command = new Deno.Command(Deno.execPath(), {
169+
args: ["test", "--no-lock", tempDirPath],
170+
});
171+
const { stdout } = await command.output();
172+
const errorMessage = stripAnsiCode(new TextDecoder().decode(stdout));
173+
174+
assertStringIncludes(errorMessage, "should fail ... FAILED");
175+
// Previously "should fail" failing caused "should pass" to fail
176+
assertStringIncludes(errorMessage, "should pass ... ok");
177+
} finally {
178+
await Deno.remove(tempDirPath, { recursive: true });
179+
}
64180
});

internal/assertion_state.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ export class AssertionState {
2626
assertionTriggered: false,
2727
assertionTriggeredCount: 0,
2828
};
29+
30+
// If any checks were registered, after the test suite runs the checks,
31+
// `resetAssertionState` should also have been called. If it was not,
32+
// then the test suite did not run the checks.
33+
globalThis.addEventListener("unload", () => {
34+
if (
35+
this.#state.assertionCheck ||
36+
this.#state.assertionCount !== undefined
37+
) {
38+
throw new Error(
39+
"AssertionCounter was not cleaned up: If tests are not otherwise failing, ensure `expect.hasAssertion` and `expect.assertions` are only run in bdd tests",
40+
);
41+
}
42+
});
2943
}
3044

3145
/**

internal/assertion_state_test.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,56 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

3-
import { assertEquals } from "@std/assert";
3+
import { assertEquals, assertStringIncludes } from "@std/assert";
44
import { AssertionState } from "./assertion_state.ts";
5+
import { stripAnsiCode } from "./styles.ts";
56

67
Deno.test("AssertionState checkAssertionErrorState pass", () => {
78
const assertionState = new AssertionState();
8-
assertionState.setAssertionTriggered(true);
9-
10-
assertEquals(assertionState.checkAssertionErrorState(), false);
9+
try {
10+
assertionState.setAssertionTriggered(true);
11+
assertEquals(assertionState.checkAssertionErrorState(), false);
12+
} finally {
13+
assertionState.resetAssertionState();
14+
}
1115
});
1216

1317
Deno.test("AssertionState checkAssertionErrorState pass", () => {
1418
const assertionState = new AssertionState();
15-
assertionState.setAssertionTriggered(true);
19+
try {
20+
assertionState.setAssertionTriggered(true);
1621

17-
assertEquals(assertionState.checkAssertionErrorState(), false);
22+
assertEquals(assertionState.checkAssertionErrorState(), false);
1823

19-
assertionState.setAssertionCheck(true);
20-
assertEquals(assertionState.checkAssertionErrorState(), false);
24+
assertionState.setAssertionCheck(true);
25+
assertEquals(assertionState.checkAssertionErrorState(), false);
26+
} finally {
27+
assertionState.resetAssertionState();
28+
}
2129
});
2230

2331
Deno.test("AssertionState checkAssertionErrorState fail", () => {
2432
const assertionState = new AssertionState();
25-
assertionState.setAssertionCheck(true);
33+
try {
34+
assertionState.setAssertionCheck(true);
35+
assertEquals(assertionState.checkAssertionErrorState(), true);
36+
} finally {
37+
assertionState.resetAssertionState();
38+
}
39+
});
2640

27-
assertEquals(assertionState.checkAssertionErrorState(), true);
41+
Deno.test("AssertionState throws if not cleaned up", async () => {
42+
const command = new Deno.Command(Deno.execPath(), {
43+
args: [
44+
"eval",
45+
`
46+
import { AssertionState } from "@std/internal/assertion-state";
47+
const assertionState = new AssertionState();
48+
assertionState.setAssertionCount(0);
49+
`,
50+
],
51+
});
52+
const { stderr } = await command.output();
53+
const errorMessage = stripAnsiCode(new TextDecoder().decode(stderr));
54+
// TODO(WWRS): Test for the expected message when Deno displays it instead of "Uncaught null"
55+
assertStringIncludes(errorMessage, "error: Uncaught");
2856
});

testing/_test_suite.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -429,19 +429,23 @@ export class TestSuiteInternal<T> implements TestSuite<T> {
429429
await fn.call(context, t);
430430
}
431431

432-
if (assertionState.checkAssertionErrorState()) {
433-
throw new AssertionError(
434-
"Expected at least one assertion to be called but received none",
435-
);
436-
}
432+
try {
433+
if (assertionState.checkAssertionErrorState()) {
434+
throw new AssertionError(
435+
"Expected at least one assertion to be called but received none",
436+
);
437+
}
437438

438-
if (assertionState.checkAssertionCountSatisfied()) {
439-
throw new AssertionError(
440-
`Expected at least ${assertionState.assertionCount} assertion to be called, ` +
441-
`but received ${assertionState.assertionTriggeredCount}`,
442-
);
439+
if (assertionState.checkAssertionCountSatisfied()) {
440+
throw new AssertionError(
441+
`Expected exactly ${assertionState.assertionCount} ${
442+
assertionState.assertionCount === 1 ? "assertion" : "assertions"
443+
} to be called, ` +
444+
`but received ${assertionState.assertionTriggeredCount}`,
445+
);
446+
}
447+
} finally {
448+
assertionState.resetAssertionState();
443449
}
444-
445-
assertionState.resetAssertionState();
446450
}
447451
}

testing/bdd.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -600,20 +600,24 @@ export function it<T>(...args: ItArgs<T>) {
600600
TestSuiteInternal.runningCount--;
601601
}
602602

603-
if (assertionState.checkAssertionErrorState()) {
604-
throw new AssertionError(
605-
"Expected at least one assertion to be called but received none",
606-
);
607-
}
603+
try {
604+
if (assertionState.checkAssertionErrorState()) {
605+
throw new AssertionError(
606+
"Expected at least one assertion to be called but received none",
607+
);
608+
}
608609

609-
if (assertionState.checkAssertionCountSatisfied()) {
610-
throw new AssertionError(
611-
`Expected at least ${assertionState.assertionCount} assertion to be called, ` +
612-
`but received ${assertionState.assertionTriggeredCount}`,
613-
);
610+
if (assertionState.checkAssertionCountSatisfied()) {
611+
throw new AssertionError(
612+
`Expected exactly ${assertionState.assertionCount} ${
613+
assertionState.assertionCount === 1 ? "assertion" : "assertions"
614+
} to be called, ` +
615+
`but received ${assertionState.assertionTriggeredCount}`,
616+
);
617+
}
618+
} finally {
619+
assertionState.resetAssertionState();
614620
}
615-
616-
assertionState.resetAssertionState();
617621
},
618622
};
619623
if (ignore !== undefined) {

0 commit comments

Comments
 (0)