From 4a595117732b4af9fc767e915f7ac06f1f20bcad Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 29 Aug 2023 01:14:26 +0900 Subject: [PATCH] :+1: Improve the assertion error message 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`. --- util.ts | 41 +++++++++++++++++++++++++++++- util_test.ts | 71 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/util.ts b/util.ts index b9987fb..65f26ac 100644 --- a/util.ts +++ b/util.ts @@ -1,5 +1,19 @@ import type { Predicate } from "./is.ts"; +export type AssertMessageFactory = ( + x: unknown, + pred: Predicate, +) => 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. */ @@ -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. * @@ -44,7 +83,7 @@ export function assert( ): asserts x is T { if (!pred(x)) { throw new AssertError( - options.message ?? "The value is not the expected type", + options.message ?? assertMessageFactory(x, pred), ); } } diff --git a/util_test.ts b/util_test.ts index 4100123..38abd00 100644 --- a/util_test.ts +++ b/util_test.ts @@ -2,24 +2,51 @@ import { assertStrictEquals, assertThrows, } from "https://deno.land/std@0.200.0/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", ); @@ -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", ); @@ -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); +});