Skip to content

Commit

Permalink
👍 Improve the assertion error message
Browse files Browse the repository at this point in the history
Now the default assertion error message contains the information about
the type of value, predicate name, and the value itself.

Additionally, users can change the factory function with `setAssertMessageFactory`.
  • Loading branch information
lambdalisue committed Aug 28, 2023
1 parent 2e392ad commit 4a59511
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 10 deletions.
41 changes: 40 additions & 1 deletion util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import type { Predicate } from "./is.ts";

export type AssertMessageFactory = (
x: unknown,
pred: Predicate<unknown>,
) => string;

export const defaultAssertMessageFactory: AssertMessageFactory = (x, pred) => {
const p = pred.name || "anonymous predicate";
const t = typeof x;
const v = JSON.stringify(x, null, 2);
return `Expected a value that satisfies the predicate ${p}, got ${t}: ${v}`;
};

let assertMessageFactory = defaultAssertMessageFactory;

/**
* Represents an error that occurs when an assertion fails.
*/
Expand All @@ -19,6 +33,31 @@ export class AssertError extends Error {
}
}

/**
* Sets the factory function used to generate assertion error messages.
* @param factory The factory function.
* @example
* ```ts
* import { setAssertMessageFactory } from "./util.ts";
* import is from "./is.ts";
*
* setAssertMessageFactory((x, pred) => {
* if (pred === is.String) {
* return `Expected a string, got ${typeof x}`;
* } else if (pred === is.Number) {
* return `Expected a number, got ${typeof x}`;
* } else if (pred === is.Boolean) {
* return `Expected a boolean, got ${typeof x}`;
* } else {
* return `Expected a value that satisfies the predicate, got ${typeof x}`;
* }
* });
* ```
*/
export function setAssertMessageFactory(factory: AssertMessageFactory): void {
assertMessageFactory = factory;
}

/**
* Asserts that the given value satisfies the provided predicate.
*
Expand All @@ -44,7 +83,7 @@ export function assert<T>(
): asserts x is T {
if (!pred(x)) {
throw new AssertError(
options.message ?? "The value is not the expected type",
options.message ?? assertMessageFactory(x, pred),
);
}
}
Expand Down
71 changes: 62 additions & 9 deletions util_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,51 @@ import {
assertStrictEquals,
assertThrows,
} from "https://deno.land/[email protected]/testing/asserts.ts";
import { assert, AssertError, ensure, maybe } from "./util.ts";
import {
assert,
AssertError,
defaultAssertMessageFactory,
ensure,
maybe,
setAssertMessageFactory,
} from "./util.ts";

const x: unknown = Symbol("x");

function truePredicate(_x: unknown): _x is string {
return true;
}

function falsePredicate(_x: unknown): _x is string {
return false;
}

Deno.test("assert", async (t) => {
await t.step("does nothing on true predicate", () => {
assert(x, (_x): _x is string => true);
assert(x, truePredicate);
});

await t.step("throws an `AssertError` on false predicate", () => {
assertThrows(() => assert(x, (_x): _x is string => false), AssertError);
assertThrows(
() => assert(x, falsePredicate),
AssertError,
`Expected a value that satisfies the predicate falsePredicate, got symbol: undefined`,
);
});

await t.step("throws an `AssertError` on false predicate", () => {
assertThrows(
() => assert(x, falsePredicate),
AssertError,
`Expected a value that satisfies the predicate falsePredicate, got symbol: undefined`,
);
});

await t.step(
"throws an `AssertError` with a custom message on false predicate",
() => {
assertThrows(
() => assert(x, (_x): _x is string => false, { message: "Hello" }),
() => assert(x, falsePredicate, { message: "Hello" }),
AssertError,
"Hello",
);
Expand All @@ -29,18 +56,22 @@ Deno.test("assert", async (t) => {

Deno.test("ensure", async (t) => {
await t.step("returns `x` as-is on true predicate", () => {
assertStrictEquals(ensure(x, (_x): _x is string => true), x);
assertStrictEquals(ensure(x, truePredicate), x);
});

await t.step("throws an `AssertError` on false predicate", () => {
assertThrows(() => ensure(x, (_x): _x is string => false), AssertError);
assertThrows(
() => ensure(x, falsePredicate),
AssertError,
`Expected a value that satisfies the predicate falsePredicate, got symbol: undefined`,
);
});

await t.step(
"throws an `AssertError` with a custom message on false predicate",
() => {
assertThrows(
() => ensure(x, (_x): _x is string => false, { message: "Hello" }),
() => ensure(x, falsePredicate, { message: "Hello" }),
AssertError,
"Hello",
);
Expand All @@ -50,10 +81,32 @@ Deno.test("ensure", async (t) => {

Deno.test("maybe", async (t) => {
await t.step("returns `x` as-is on true predicate", () => {
assertStrictEquals(maybe(x, (_x): _x is string => true), x);
assertStrictEquals(maybe(x, truePredicate), x);
});

await t.step("returns `undefined` on false predicate", () => {
assertStrictEquals(maybe(x, (_x): _x is string => false), undefined);
assertStrictEquals(maybe(x, falsePredicate), undefined);
});
});

Deno.test("setAssertMessageFactory", async (t) => {
setAssertMessageFactory((x, pred) => `Hello ${typeof x} ${pred.name}`);

await t.step("change `AssertError` message on `assert` failure", () => {
assertThrows(
() => assert(x, falsePredicate),
AssertError,
"Hello symbol falsePredicate",
);
});

await t.step("change `AssertError` message on `ensure` failure", () => {
assertThrows(
() => ensure(x, falsePredicate),
AssertError,
"Hello symbol falsePredicate",
);
});

setAssertMessageFactory(defaultAssertMessageFactory);
});

0 comments on commit 4a59511

Please sign in to comment.