Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(expect,internal,testing): support expect.assertions #6032

Merged
merged 26 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
18390e5
feat: expect.hasAssertion api
eryue0220 Sep 4, 2024
f61d3ca
Merge branch 'main' into feat/expect-assertions-api
eryue0220 Sep 4, 2024
dd1c658
fix: test and lint
eryue0220 Sep 4, 2024
60f4d84
fix: jsdoc ci
eryue0220 Sep 4, 2024
ba22b40
Merge branch 'main' into feat/expect-assertions-api
eryue0220 Sep 4, 2024
c569cfd
fix: typo
eryue0220 Sep 4, 2024
cd25d19
fix: ci
eryue0220 Sep 4, 2024
74988ae
Merge branch 'main' into feat/expect-assertions-api
eryue0220 Sep 4, 2024
22ce01b
feat: add expect api
eryue0220 Sep 9, 2024
be946a0
Apply suggestions from code review
kt3k Sep 9, 2024
31e84ef
chore: add note about unstableness
kt3k Sep 9, 2024
450b702
fix: bug
eryue0220 Sep 10, 2024
415be75
Merge branch 'main' into feat/expect-assertions-api
eryue0220 Sep 10, 2024
179a086
feat: add new api
eryue0220 Sep 10, 2024
cef4b55
feat: expect.assertions api
eryue0220 Sep 22, 2024
b1b90a4
Merge branch 'main' into feat/expect-assertions-count-api
eryue0220 Sep 22, 2024
33d9a44
chore: remove duplicate
eryue0220 Sep 22, 2024
bade6e5
fix: test
eryue0220 Sep 22, 2024
257ed06
fix: lint
eryue0220 Sep 22, 2024
956f022
fix: lint
eryue0220 Sep 22, 2024
9e4327a
Merge branch 'main' into feat/expect-assertions-count-api
iuioiua Sep 23, 2024
faffabd
Merge branch 'main' into feat/expect-assertions-count-api
kt3k Oct 22, 2024
f932dc6
fix: cr
eryue0220 Oct 22, 2024
f2c5910
Merge branch 'main' into feat/expect-assertions-count-api
eryue0220 Oct 22, 2024
0f610e1
update missing api
eryue0220 Oct 22, 2024
507ffe8
cancel format fix for assert
kt3k Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assert/is_error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function assertIsError<E extends Error = Error>(
const msgSuffix = msg ? `: ${msg}` : ".";
if (!(error instanceof Error)) {
throw new AssertionError(
`Expected "error" to be an Error object${msgSuffix}}`,
`Expected "error" to be an Error object${msgSuffix}`,
);
}
if (ErrorClass && !(error instanceof ErrorClass)) {
Expand Down
5 changes: 5 additions & 0 deletions expect/_assertion.ts → expect/_assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
28 changes: 26 additions & 2 deletions expect/_assertion_test.ts → expect/_assertions_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { expect } from "./expect.ts";

Deno.test("expect.hasAssertions() API", () => {
describe("describe suite", () => {
// FIXME(eryue0220): This test should throw `toThrowErrorMatchingSnapshot`
// FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot`
it("should throw an error", () => {
expect.hasAssertions();
});
Expand All @@ -21,7 +21,7 @@ Deno.test("expect.hasAssertions() API", () => {
expect("a").toEqual("a");
});

// FIXME(eryue0220): This test should throw `toThrowErrorMatchingSnapshot`
// FIXME(eryue0220): This test should through `toThrowErrorMatchingSnapshot`
test("test suite should throw an error", () => {
expect.hasAssertions();
});
Expand All @@ -31,3 +31,27 @@ Deno.test("expect.hasAssertions() API", () => {
expect("a").toEqual("a");
});
});

Deno.test("expect.assertions() API", () => {
test("should pass", () => {
expect.assertions(2);
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);
});
});
26 changes: 25 additions & 1 deletion expect/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -491,6 +495,7 @@ expect.stringContaining = asymmetricMatchers.stringContaining as (
expect.stringMatching = asymmetricMatchers.stringMatching as (
pattern: string | RegExp,
) => ReturnType<typeof asymmetricMatchers.stringMatching>;

/**
* `expect.hasAssertions` verifies that at least one assertion is called during a test.
*
Expand All @@ -509,3 +514,22 @@ 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;
121 changes: 112 additions & 9 deletions internal/assertion_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
Expand All @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems in jest, expect.assertions(N) call expects exactly N assertions are made. For example, the below test file fails with the error message Expected one assertion to be called but received two assertion calls.:

test("test", () => {
  expect.assertions(1);
  expect(1).toBe(1);
  expect(2).toBe(2);
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check this later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I've updated.

}
}

const assertionState = new AssertionState();
Expand Down
14 changes: 7 additions & 7 deletions internal/assertion_state_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
21 changes: 21 additions & 0 deletions testing/_test_suite.ts
Original file line number Diff line number Diff line change
@@ -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<T> extends Omit<Deno.TestDefinition, "fn"> {
/** The body of the test suite */
Expand Down Expand Up @@ -383,5 +389,20 @@ export class TestSuiteInternal<T> implements TestSuite<T> {
} 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();
}
}
11 changes: 10 additions & 1 deletion testing/bdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,11 +598,20 @@ export function it<T>(...args: ItArgs<T>) {
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) {
Expand Down