ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Remix 断言库 remix/assert 完整指南:从 node:assert/strict 兼容 API 到 expect 链式匹配器

Remix 断言库 remix/assert 完整指南:从 node:assert/strict 兼容 API 到 expect 链式匹配器 Remix 断言库 remix/assert 完整指南从 node:assert/strict 兼容 API 到 expect 链式匹配器【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remixremix/assert是 Remix 全栈框架当前仓库packages/assert内置的一套跨环境断言库它实现了node:assert/strict的兼容子集可在浏览器、Node.js、Bun 等任何 JavaScript 运行时中运行同时提供 vitest/jest 风格的expect链式匹配器 API。阅读本文后你将掌握这套断言库的全部 API、Object.is严格相等的边界语义、深度相等引擎的内部原理以及如何用它编写同步与异步测试断言。一、定位与设计理念packages/assert包发布名为remix-run/assert见 package.json的设计目标非常明确兼容子集提供与node:assert/strict行为一致的核心断言函数但不依赖 Node.js 运行时因此天然适用于浏览器、Worker、Deno、Bun 等任何 JS 环境统一错误类型所有断言失败时抛出同一个AssertionError其结构与node:assert.AssertionError兼容包含actual、expected、operator、name等字段测试报告器可以统一处理严格相等所有比较一律基于Object.is不做任何类型强制转换双 API 形态既保留命令式的assert.xxx()调用方式又提供链式expect(value).toBe()匹配器风格覆盖不同团队的测试习惯。在框架内remix/assert通过 packages/remix/src/assert.ts 一行 re-exportexport * from remix-run/assert暴露为remix/assert子路径因此用户既可以安装独立的remix-run/assert包也可以直接从remix/assert引入。二、安装与引入npm i remix安装后即可从remix/assert引入。它同时提供默认导出与命名导出两种形态。默认导出行为与node:assert/strict一致import assert from remix/assert assert.ok(true) assert(true) // assert 本身就是 ok 的别名可直接调用命名导出每个断言函数都可以单独引入见 src/index.tsimport { ok, assert, // ok() 的别名 equal, notEqual, deepEqual, partialDeepEqual, notDeepEqual, match, doesNotMatch, fail, throws, doesNotThrow, rejects, doesNotReject, } from remix/assert从源码看默认导出的对象通过Object.assign(assertFn, { ... })将所有断言方法挂载到ok函数上src/index.ts因此assert.equal、assert.deepEqual等属性访问方式同样可用。三、核心语义一切比较都基于 Object.is本库严格镜像node:assert/strict所有相等判断使用Object.is见 src/lib/assert.ts 中equal的实现if (!Object.is(actual, expected))即抛错。这意味着以下边界行为与直觉不同需要特别留意比较表达式Object.is结果说明1与1不相等数字与字符串属于不同类型不比较值null与undefined不相等两者不同NaN与NaN相等NaN ! NaN是的坑但Object.is正确返回 true0与-0不相等认为相等但Object.is区分正负零这些语义在 src/lib/assert.test.ts 中有直接验证assert.equal(1, 1)抛AssertionError、assert.equal(0, -0)抛错、assert.equal(NaN, NaN)通过。3.1 完整基础用法示例import assert from remix/assert assert.ok(true) assert(true) // assert 是 ok 的别名 assert.equal(1, 1) assert.equal(1, 1) // 抛错 —— 类型不同 assert.equal(NaN, NaN) // 通过 —— Object.is(NaN, NaN) true assert.notEqual(a, b) assert.deepEqual({ a: 1 }, { a: 1 }) assert.deepEqual({ a: 1 }, { a: 1 }) // 抛错 —— 类型不同不强制转换 assert.partialDeepEqual({ a: 1, b: 2 }, { a: 1 }) // 部分匹配通过 assert.match(hello world, /world/) assert.fail(should not reach here) // 无条件失败3.2 错误消息参数所有断言函数都接受可选的message参数类型为string | Error。注意源码中的细节src/lib/assert.ts比较型断言equal、deepEqual等若传入的是Error实例则直接抛出该 Error测试中验证了assert.equal(1, 2, new Error(custom))会抛出这个自定义 Error未传message时自动生成可读性强的消息例如assert.equal(1, 1)失败消息为1 ! 1AssertionError上generatedMessage字段标记消息是否为自动生成。3.3 类型收窄TypeScript 友好ok与equal使用了 TypeScript 的asserts类型谓词src/lib/assert.ts 中export function ok(value: unknown, message?: AssertionMessage): asserts value调用之后编译器能自动收窄变量类型const cookie getSessionCookie(response) // 类型为 string | null assert.ok(cookie) // 之后 cookie 的类型收窄为 string同理assert.equal(response.status, 200)之后response.status的类型会被收窄为字面量200。这是本库在开发时充当运行时契约校验的重要价值既在运行时兜底又让静态类型推导受益。四、AssertionError统一错误对象所有断言失败都会抛出AssertionErrorsrc/lib/assert.ts其字段与 Node.js 内建assert.AssertionError对齐字段含义name固定为AssertionErrormessage失败消息自定义或自动生成actual实际值expected期望值operator触发失败的断言操作符如strictEqual、deepStrictEqual、throws、partialDeepStrictEqual等generatedMessage消息是否由库自动生成code固定为ERR_ASSERTION与 Node 保持一致operator字段是测试报告器和调试工具区分失败原因的关键例如equal对应strictEqualnotEqual对应notStrictEqual。测试 src/lib/assert.test.ts 完整验证了这些字段的设置行为。五、深度相等引擎deepEqual 与 partialDeepEqualdeepEqual/notDeepEqual/partialDeepEqual是assert库的核心能力其递归比较逻辑独立实现于 src/lib/deep-equal.ts采用full完全相等与partial部分匹配两种模式。理解它的底层规则对写出准确的断言至关重要。5.1 完全模式deepEqual的比较规则递归比较函数compare的判定顺序src/lib/deep-equal.ts先做Object.is快速路径基本类型直接按严格相等判定类型一致性typeof actual ! typeof expected直接失败函数与函数之间恒不等不比较函数体原型链完全模式下要求Object.getPrototypeOf一致Object.create(null)与{}因此不相等内建类型逐一特判Date比较getTime()时间戳RegExp比较source、flags以及lastIndex带g标志的正则状态也参与比较测试 src/lib/assert.test.ts 验证了lastIndex不同则失败Error比较name、message并递归比较cause与errorsAggregateError属性Map/Set元素顺序无关但内容必须一一对应ArrayBuffer/SharedArrayBuffer/ TypedArray按字节比较TypedArray 还要求构造函数一致装箱基本类型Boolean、Number、String、BigInt、Symbol比较valueOf()结果URL比较序列化后的完整字符串Promise、WeakMap、WeakSet恒不相等不可遍历内容普通对象枚举键包括 Symbol 键逐一递归比较键集合必须一致循环引用通过WeakMap记录已比较的对hasCompared/rememberComparison{ a: 1, self: obj }这类自引用对象可以安全比较而不死循环见测试 src/lib/assert.test.ts。5.2 部分模式partialDeepEqual的扩展语义partialDeepEqual(actual, expected)断言actual中包含expected所描述的部分结构允许actual有多余内容。在源码中体现为src/lib/deep-equal.ts对象expected的每个键必须存在于actual且递归匹配actual可以多键数组子序列匹配——[1,2,3,4,5,6,7,8,9]可以部分匹配[4,5,8]但[1,2,3]不能匹配[2,1]顺序必须保持见测试 src/lib/assert.test.ts字节序列Uint8Array/ArrayBuffer同样支持子序列匹配Map/Set支持部分键 部分值的子集匹配Errorcause、errors等属性可以部分匹配。assert.partialDeepEqual({ a: 1, b: 2 }, { a: 1 }) // 通过 assert.partialDeepEqual([1, 2, 3, 4, 5], [2, 4]) // 通过子序列 assert.partialDeepEqual({ a: { b: 1 } }, { a: { b: 2 } }) // 抛错 —— 值不匹配 assert.partialDeepEqual({}, { a: undefined }) // 抛错 —— 键必须存在最后一个例子值得注意undefined值并不豁免键的存在性检查这与toEqual(expect.objectContaining({ a: undefined }))的行为一致见 src/lib/expect.test.ts。六、match / doesNotMatch字符串正则断言assert.match(hello world, /world/) // 通过 assert.doesNotMatch(html, /Error/) // 断言不匹配源码中checkMatchArgumentssrc/lib/assert.ts会先校验regexp必须是RegExp实例否则抛ERR_INVALID_ARG_TYPE再校验string参数类型。doesNotMatch在断言失败时输出${stringify(string)} matches ${regexp}的失败消息。七、throws / rejects错误抛出与异步拒绝断言这是本库最复杂的部分assert.throws与assert.rejects共享同一套错误匹配器逻辑checkErrorsrc/lib/assert.tsexpectedError参数支持五种形态形态匹配规则Error 构造函数被抛出的错误必须是该构造函数的实例error instanceof CtorError 实例与被抛错误做深度相等比较RegExp对错误对象的字符串表示做test匹配相当于匹配错误消息普通对象逐属性校验每个属性必须与错误的同名属性深度相等属性值为RegExp时则要求错误对应属性是字符串且匹配该正则验证函数以错误为参数调用必须严格返回true才算匹配与 Node 的断言契约一致返回其他 truthy 值同样视为失败7.1 throws 基本用法assert.throws(() { throw new TypeError(bad) }, TypeError) // 按构造函数匹配 assert.throws( () { let error new Error(Invalid value) as Error { code: string } error.code ERR_INVALID_ARG_VALUE throw error }, { code: ERR_INVALID_ARG_VALUE, message: /Invalid value/ }, ) // 按对象逐属性匹配code 严格相等message 用正则错误对象形态下还内置了一个校验expectedError不能是空对象否则抛出ERR_INVALID_ARG_VALUEsrc/lib/assert.ts。参数解析还有一个便利点parseExpectedErrorsrc/lib/assert.ts如果第二个参数是字符串且未传第三个参数则该字符串被当作失败消息而非错误匹配器。7.2 rejects 异步用法rejects接受返回 Promise 的函数或Promise 本身两种形式getPromise会校验返回值类型函数形式必须返回 Promise 实例否则抛ERR_INVALID_RETURN_VALUE见 src/lib/assert.tsawait assert.rejects(() Promise.reject(new Error(oops))) await assert.rejects(fetch(/missing), (err) err.status 404) // 验证函数 await assert.rejects(fetch(/missing), { code: ERR_INVALID_ARG_VALUE }) // 对象匹配7.3 doesNotThrow / doesNotReject反向断言doesNotThrow(fn)期望函数不抛错doesNotReject(fn)期望 Promise 不拒绝。当错误确实发生时失败消息会附带实际错误内容Got unwanted exception.\nActual message: ...。若指定了expectedError且不匹配原错误会被重新抛出而非吞掉src/lib/assert.ts。八、expectvitest/jest 风格的链式匹配器expectAPI 构建在同一套AssertionError之上src/lib/expect.ts提供与 vitest/jest 几乎一致的链式体验支持.not取反、.rejects/.resolves处理 Promise并内置了 mock 感知匹配器。8.1 快速上手import { expect } from remix/assert expect(value).toBe(42) expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 2 }) expect({ a: 1, b: 2 }).toEqual(expect.objectContaining({ a: 1 })) // 非对称匹配 expect({ a: { b: 1, c: 2 } }).toMatchObject({ a: { b: 1 } }) // 递归部分匹配 expect(value).not.toBeNull() expect(arr).toHaveLength(3) expect(spy).toHaveBeenCalledWith(hello, 1) await expect(fetch(/missing)).rejects.toThrow(Not found) await expect(loadModule()).resolves.toBeUndefined()8.2 完整匹配器清单分组匹配器相等性toBe、toEqual、toBeNull、toBeUndefined、toBeDefined、toBeTruthy、toBeInstanceOf数值toBeGreaterThan、toBeGreaterThanOrEqual、toBeLessThan、toBeLessThanOrEqual、toBeCloseTo字符串/可迭代toContain、toMatch、toHaveLength对象形态toHaveProperty(path, value?)、toMatchObject(partial)抛错toThrow(expected?)Mock 感知toHaveBeenCalled、toHaveBeenCalledTimes(n)、toHaveBeenCalledWith(...args)、toHaveBeenNthCalledWith(nth, ...args)几个实现细节来自 src/lib/expect.tstoBe基于Object.is严格相等NaN等于NaNtoEqual内部复用深度相等引擎但额外识别expect.objectContaining生成的非对称匹配器toMatch接受RegExp或字符串字符串会被escapeRegex转义后作为字面量正则toMatch(hello)相当于字面包含匹配toContain支持字符串子串包含、数组 /Set/ 任意可迭代对象元素用Object.is或比较toHaveProperty支持点号路径a.b.c可选第二个参数做值校验toBeCloseTo(n, precision 2)默认精度为 2容差为10^-precision / 2toThrow(expected)的expected支持 Error 构造函数、Error 实例按 message 匹配、RegExp、字符串message 包含以及验证函数。8.3 非对称匹配器 expect.objectContainingexpect({ a: 1, b: 2 }).toEqual(expect.objectContaining({ a: 1 }))objectContaining(partial)返回一个带有Symbol.for(remix-run/assert/partialMatcher)哨兵标记的对象src/lib/expect.ts。toEqual以及任何底层使用深度相等的匹配器在expected位置遇到该标记时会切换到matchesPartial递归部分匹配逻辑要求actual至少包含partial中的键且值匹配额外键被允许。注意partial中值为undefined的键同样要求真实存在。8.4 异步匹配器rejects / resolves.rejects与.resolves是异步门控createAsyncMatcherssrc/lib/expect.ts先await被断言的值函数或 Promise再对结果运行后续匹配器。若方向错误——例如rejects却解析成功——会抛出明确的AssertionErrorexpected promise to reject, but it resolved with: ...其中rejects.toThrow有专门处理直接对被拒绝的错误执行checkErrorMatch其余匹配器则对错误值本身执行。使用这些异步匹配器时记得await。8.5 Mock 感知匹配器与 remix/test 联动toHaveBeenCalled*系列匹配器读取received.mock.calls[i].argumentsgetMockCallssrc/lib/expect.ts这正是mock.fn()/mock.method()产生的数据结构来自remix/test即 packages/test。如果传入的值不是带.mock.calls数组的 mock 函数会直接抛错提示toHaveBeenCalled requires a mock function with a .mock.calls propertyimport { expect } from remix/assert import { mock } from remix/test const spy mock.fn() spy(hello, 1) expect(spy).toHaveBeenCalled() expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledWith(hello, 1) expect(spy).toHaveBeenNthCalledWith(1, hello, 1)toHaveBeenCalledWith对每次调用的参数做深度相等比较因此可以传入对象、数组等结构化参数。九、测试与验证仓库中的行为证据本库的行为由两套测试直接保障均使用 Node 内置的node:test运行器npm test对应node --test见 package.json同时提供test:bun供 Bun 运行src/lib/assert.test.ts806 行覆盖AssertionError字段、默认导出的可调用性与属性挂载、equal的严格相等边界含0vs-0、NaN、deepEqual的内建类型比较Date/RegExp/Error/Map/Set/TypedArray/Symbol 键/原型/循环引用、partialDeepEqual的子序列与部分匹配、throws/rejects的错误匹配器矩阵、doesNotThrow/doesNotReject的反向语义等src/lib/expect.test.ts320 行覆盖toBe/toEqual的严格语义、objectContaining部分匹配、toBeNull系列、toHaveProperty、toMatchObject、mock 匹配器等且大量用例直接与node:assert/strict交叉验证行为一致性。这些测试同时验证了一个关键设计目标行为与 Node.js 内置断言保持对齐——例如assert.deepEqual({ a: undefined }, { b: undefined })在 Node 严格模式下抛错本库同样抛错。十、适用场景小结在 Remix 框架的生态中remix/assert主要服务于三个场景跨运行时测试同一套断言代码可以在 Node、Bun、浏览器测试环境中运行无需为环境切换断言库运行时防御利用assert.ok/assert.equal的asserts类型谓词在函数入口做契约校验既抛错又收窄类型统一测试风格团队成员可以自由选择命令式assert.xxx或链式expect().toBe风格失败错误统一为兼容 Node 的AssertionError报告器无需特殊适配。从依赖角度看该包零运行时依赖devDependencies 仅有types/node与typescript见 package.json体积与兼容性俱佳是 Remix 全栈框架中基础但不可或缺的设施。相关源码与文档索引包入口与导出packages/assert/src/index.ts断言函数实现packages/assert/src/lib/assert.ts深度相等引擎packages/assert/src/lib/deep-equal.tsexpect 匹配器实现packages/assert/src/lib/expect.ts行为测试packages/assert/src/lib/assert.test.ts、packages/assert/src/lib/expect.test.ts框架内 re-exportpackages/remix/src/assert.ts包元信息与许可证packages/assert/package.json、packages/assert/LICENSE【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进