diff --git a/expect/_assertion_test.ts b/expect/_assertion_test.ts deleted file mode 100644 index 8a3cfd966ce4..000000000000 --- a/expect/_assertion_test.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -import { describe, it, test } from "@std/testing/bdd"; -import { expect } from "./expect.ts"; - -Deno.test("expect.hasAssertions() API", () => { - describe("describe suite", () => { - // FIXME(eryue0220): This test should throw `toThrowErrorMatchingSnapshot` - it("should throw an error", () => { - expect.hasAssertions(); - }); - - it("should pass", () => { - expect.hasAssertions(); - expect("a").toEqual("a"); - }); - }); - - it("it() suite should pass", () => { - expect.hasAssertions(); - expect("a").toEqual("a"); - }); - - // FIXME(eryue0220): This test should throw `toThrowErrorMatchingSnapshot` - test("test suite should throw an error", () => { - expect.hasAssertions(); - }); - - test("test suite should pass", () => { - expect.hasAssertions(); - expect("a").toEqual("a"); - }); -}); diff --git a/expect/_assertion.ts b/expect/_assertions.ts similarity index 72% rename from expect/_assertion.ts rename to expect/_assertions.ts index 7540c27b075e..40224d1e9abb 100644 --- a/expect/_assertion.ts +++ b/expect/_assertions.ts @@ -8,6 +8,11 @@ export function hasAssertions() { assertionState.setAssertionCheck(true); } +export function assertions(num: number) { + assertionState.setAssertionCount(num); +} + export function emitAssertionTrigger() { assertionState.setAssertionTriggered(true); + assertionState.updateAssertionTriggerCount(); } diff --git a/expect/_assertions_test.ts b/expect/_assertions_test.ts new file mode 100644 index 000000000000..e5cc09fb9cf3 --- /dev/null +++ b/expect/_assertions_test.ts @@ -0,0 +1,64 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { describe, it, test } from "@std/testing/bdd"; +import { expect } from "./expect.ts"; + +Deno.test("expect.hasAssertions() API", () => { + describe("describe suite", () => { + // FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot` + it("should throw an error", () => { + expect.hasAssertions(); + }); + + it("should pass", () => { + expect.hasAssertions(); + expect("a").toEqual("a"); + }); + }); + + it("it() suite should pass", () => { + expect.hasAssertions(); + expect("a").toEqual("a"); + }); + + // FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot` + test("test suite should throw an error", () => { + expect.hasAssertions(); + }); + + test("test suite should pass", () => { + expect.hasAssertions(); + expect("a").toEqual("a"); + }); +}); + +Deno.test("expect.assertions() API", () => { + test("should pass", () => { + expect.assertions(2); + expect("a").not.toBe("b"); + expect("a").toBe("a"); + }); + + // FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot` + test("should throw error", () => { + expect.assertions(1); + expect("a").not.toBe("b"); + expect("a").toBe("a"); + }); + + it("redeclare different assertion count", () => { + expect.assertions(3); + expect("a").not.toBe("b"); + expect("a").toBe("a"); + expect.assertions(2); + }); + + test("expect no assertions", () => { + expect.assertions(0); + }); + + // FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot` + it("should throw an error", () => { + expect.assertions(2); + }); +}); diff --git a/expect/expect.ts b/expect/expect.ts index 30e76334675d..924b6a86b153 100644 --- a/expect/expect.ts +++ b/expect/expect.ts @@ -14,7 +14,11 @@ import type { Matchers, } from "./_types.ts"; import { AssertionError } from "@std/assert/assertion-error"; -import { emitAssertionTrigger, hasAssertions } from "./_assertion.ts"; +import { + assertions, + emitAssertionTrigger, + hasAssertions, +} from "./_assertions.ts"; import { addCustomEqualityTesters, getCustomEqualityTesters, @@ -491,6 +495,7 @@ expect.stringContaining = asymmetricMatchers.stringContaining as ( expect.stringMatching = asymmetricMatchers.stringMatching as ( pattern: string | RegExp, ) => ReturnType; + /** * `expect.hasAssertions` verifies that at least one assertion is called during a test. * @@ -509,6 +514,26 @@ expect.stringMatching = asymmetricMatchers.stringMatching as ( * ``` */ expect.hasAssertions = hasAssertions as () => void; + +/** + * `expect.assertions` verifies that a certain number of assertions are called during a test. + * + * Note: expect.assertions only can use in bdd function test suite, such as `test` or `it`. + * + * @example + * ```ts + * + * import { test } from "@std/testing/bdd"; + * import { expect } from "@std/expect"; + * + * test("it works", () => { + * expect.assertions(1); + * expect("a").not.toBe("b"); + * }); + * ``` + */ +expect.assertions = assertions as (num: number) => void; + /** * `expect.objectContaining(object)` matches any received object that recursively matches the expected properties. * That is, the expected object is not a subset of the received object. Therefore, it matches a received object diff --git a/expect/mod.ts b/expect/mod.ts index 65cda1274428..cb0a9f1fe5ea 100644 --- a/expect/mod.ts +++ b/expect/mod.ts @@ -59,6 +59,7 @@ * - {@linkcode expect.stringContaining} * - {@linkcode expect.stringMatching} * - Utilities: + * - {@linkcode expect.assertions} * - {@linkcode expect.addEqualityTester} * - {@linkcode expect.extend} * - {@linkcode expect.hasAssertions} @@ -72,7 +73,6 @@ * - Asymmetric matchers: * - `expect.not.objectContaining` * - Utilities: - * - `expect.assertions` * - `expect.addSnapshotSerializer` * * The tracking issue to add support for unsupported parts of the API is diff --git a/internal/assertion_state.ts b/internal/assertion_state.ts index 4a46aceedd1e..13c37e0df6f6 100644 --- a/internal/assertion_state.ts +++ b/internal/assertion_state.ts @@ -12,17 +12,55 @@ */ export class AssertionState { #state: { + assertionCount: number | undefined; assertionCheck: boolean; assertionTriggered: boolean; + assertionTriggeredCount: number; }; constructor() { this.#state = { + assertionCount: undefined, assertionCheck: false, assertionTriggered: false, + assertionTriggeredCount: 0, }; } + /** + * Get the number that through `expect.assertions` api set. + * + * @returns the number that through `expect.assertions` api set. + * + * @example Usage + * ```ts ignore + * import { AssertionState } from "@std/internal"; + * + * const assertionState = new AssertionState(); + * assertionState.assertionCount; + * ``` + */ + get assertionCount(): number | undefined { + return this.#state.assertionCount; + } + + /** + * Get a certain number that assertions were called before. + * + * @returns return a certain number that assertions were called before. + * + * @example Usage + * ```ts ignore + * import { AssertionState } from "@std/internal"; + * + * const assertionState = new AssertionState(); + * assertionState.assertionTriggeredCount; + * ``` + */ + get assertionTriggeredCount(): number { + return this.#state.assertionTriggeredCount; + } + /** * If `expect.hasAssertions` called, then through this method to update #state.assertionCheck value. * @@ -57,6 +95,41 @@ export class AssertionState { this.#state.assertionTriggered = val; } + /** + * If `expect.assertions` called, then through this method to update #state.assertionCheck value. + * + * @param num Set #state.assertionCount's value, for example if the value is set 2, that means + * you must have two assertion matchers call in your test suite. + * + * @example Usage + * ```ts ignore + * import { AssertionState } from "@std/internal"; + * + * const assertionState = new AssertionState(); + * assertionState.setAssertionCount(2); + * ``` + */ + setAssertionCount(num: number) { + this.#state.assertionCount = num; + } + + /** + * If any matchers was called, `#state.assertionTriggeredCount` value will plus one internally. + * + * @example Usage + * ```ts ignore + * import { AssertionState } from "@std/internal"; + * + * const assertionState = new AssertionState(); + * assertionState.updateAssertionTriggerCount(); + * ``` + */ + updateAssertionTriggerCount() { + if (this.#state.assertionCount !== undefined) { + this.#state.assertionTriggeredCount += 1; + } + } + /** * Check Assertion internal state, if `#state.assertionCheck` is set true, but * `#state.assertionTriggered` is still false, then should throw an Assertion Error. @@ -69,26 +142,56 @@ export class AssertionState { * import { AssertionState } from "@std/internal"; * * const assertionState = new AssertionState(); - * if (assertionState.checkAssertionErrorStateAndReset()) { + * if (assertionState.checkAssertionErrorState()) { * // throw AssertionError(""); * } * ``` */ - checkAssertionErrorStateAndReset(): boolean { - const result = this.#state.assertionCheck && - !this.#state.assertionTriggered; - - this.#resetAssertionState(); - - return result; + checkAssertionErrorState(): boolean { + return this.#state.assertionCheck && !this.#state.assertionTriggered; } - #resetAssertionState(): void { + /** + * Reset all assertion state when every test suite function ran completely. + * + * @example Usage + * ```ts ignore + * import { AssertionState } from "@std/internal"; + * + * const assertionState = new AssertionState(); + * assertionState.resetAssertionState(); + * ``` + */ + resetAssertionState(): void { this.#state = { + assertionCount: undefined, assertionCheck: false, assertionTriggered: false, + assertionTriggeredCount: 0, }; } + + /** + * Check Assertion called state, if `#state.assertionCount` is set to a number value, but + * `#state.assertionTriggeredCount` is less then it, then should throw an assertion error. + * + * @returns a boolean value, that the test suite is satisfied with the check. If not, + * it should throw an AssertionError. + * + * @example Usage + * ```ts ignore + * import { AssertionState } from "@std/internal"; + * + * const assertionState = new AssertionState(); + * if (assertionState.checkAssertionCountSatisfied()) { + * // throw AssertionError(""); + * } + * ``` + */ + checkAssertionCountSatisfied(): boolean { + return this.#state.assertionCount !== undefined && + this.#state.assertionCount !== this.#state.assertionTriggeredCount; + } } const assertionState = new AssertionState(); diff --git a/internal/assertion_state_test.ts b/internal/assertion_state_test.ts index 4383f36c5016..f15b9e705507 100644 --- a/internal/assertion_state_test.ts +++ b/internal/assertion_state_test.ts @@ -3,26 +3,26 @@ import { assertEquals } from "@std/assert"; import { AssertionState } from "./assertion_state.ts"; -Deno.test("AssertionState checkAssertionErrorStateAndReset pass", () => { +Deno.test("AssertionState checkAssertionErrorState pass", () => { const assertionState = new AssertionState(); assertionState.setAssertionTriggered(true); - assertEquals(assertionState.checkAssertionErrorStateAndReset(), false); + assertEquals(assertionState.checkAssertionErrorState(), false); }); -Deno.test("AssertionState checkAssertionErrorStateAndReset pass", () => { +Deno.test("AssertionState checkAssertionErrorState pass", () => { const assertionState = new AssertionState(); assertionState.setAssertionTriggered(true); - assertEquals(assertionState.checkAssertionErrorStateAndReset(), false); + assertEquals(assertionState.checkAssertionErrorState(), false); assertionState.setAssertionCheck(true); - assertEquals(assertionState.checkAssertionErrorStateAndReset(), true); + assertEquals(assertionState.checkAssertionErrorState(), false); }); -Deno.test("AssertionState checkAssertionErrorStateAndReset fail", () => { +Deno.test("AssertionState checkAssertionErrorState fail", () => { const assertionState = new AssertionState(); assertionState.setAssertionCheck(true); - assertEquals(assertionState.checkAssertionErrorStateAndReset(), true); + assertEquals(assertionState.checkAssertionErrorState(), true); }); diff --git a/testing/_test_suite.ts b/testing/_test_suite.ts index bd74f85dd956..727f8a1d122a 100644 --- a/testing/_test_suite.ts +++ b/testing/_test_suite.ts @@ -1,4 +1,10 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { getAssertionState } from "@std/internal/assertion-state"; +import { AssertionError } from "@std/assert/assertion-error"; + +const assertionState = getAssertionState(); + /** The options for creating a test suite with the describe function. */ export interface DescribeDefinition extends Omit { /** The body of the test suite */ @@ -383,5 +389,20 @@ export class TestSuiteInternal implements TestSuite { } else { await fn.call(context, t); } + + if (assertionState.checkAssertionErrorState()) { + throw new AssertionError( + "Expected at least one assertion to be called but received none", + ); + } + + if (assertionState.checkAssertionCountSatisfied()) { + throw new AssertionError( + `Expected at least ${assertionState.assertionCount} assertion to be called, ` + + `but received ${assertionState.assertionTriggeredCount}`, + ); + } + + assertionState.resetAssertionState(); } } diff --git a/testing/bdd.ts b/testing/bdd.ts index 3258d46f49b5..b5d86c1cf7f0 100644 --- a/testing/bdd.ts +++ b/testing/bdd.ts @@ -598,11 +598,20 @@ export function it(...args: ItArgs) { TestSuiteInternal.runningCount--; } - if (assertionState.checkAssertionErrorStateAndReset()) { + if (assertionState.checkAssertionErrorState()) { throw new AssertionError( "Expected at least one assertion to be called but received none", ); } + + if (assertionState.checkAssertionCountSatisfied()) { + throw new AssertionError( + `Expected at least ${assertionState.assertionCount} assertion to be called, ` + + `but received ${assertionState.assertionTriggeredCount}`, + ); + } + + assertionState.resetAssertionState(); }, }; if (ignore !== undefined) {