十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Remix 测试模式指南:用 `remix test` 覆盖 HTTP 层与 DOM 层的完整实战

Remix 测试模式指南:用 `remix test` 覆盖 HTTP 层与 DOM 层的完整实战 Remix 测试模式指南用remix test覆盖 HTTP 层与 DOM 层的完整实战【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remixRemix 应用的大部分逻辑都分布在两个层次上路由/中间件组成的 HTTP 层以及组件构成的 DOM 层。本指南围绕仓库中remix测试体系的核心模式展开讲解如何用router.fetch(new Request(...))驱动路由器并断言Response如何用render(...)/createRoot(...)把组件渲染进真实 DOM 进行交互测试以及如何通过remix test配置发现规则、排除规则与覆盖率。读完本文你将掌握为 Remix 应用编写服务端路由测试与组件测试的完整套路并了解remix routes、remix doctor、remix version等 CLI 辅助命令的配合用法。本文内容以 .agents/skills/remix/references/testing-patterns.md 为骨架并结合仓库中packages/test、packages/ui、packages/node-fetch-server等包的源码与demos/bookstore的真实测试用例进行验证与扩充。适用范围与前置阅读这份测试指南解决的是「该测哪一层、怎么搭测试环境、怎么配置运行器」这三类问题当任务涉及以下场景时直接对照使用用router.fetch(new Request(...))驱动路由器并对返回的Response做断言每个测试或每个测试套件新建路由器实例以实现 session、存储、数据库的隔离用render(...)或createRoot(...)把组件渲染进真实 DOM配置remix test的测试发现discovery、排除规则excludes与覆盖率coverage使用remix routes、remix doctor、remix version等相邻 CLI 检查命令判断某个行为到底应该用哪一层测试。会话session与认证相关的测试搭建详见 auth-and-sessions.md组件生命周期相关主题见 component-model.md。两种测试形态Remix 的测试统一通过remix test运行测试框架来自remix/test断言库来自remix/assert。围绕「代码住哪一层」测试天然分成两种形态形态驱动方式环境适用行为服务端 / 路由器测试router.fetch(new Request(...))断言返回的Response无 DOM、无浏览器路由分发、中间件、session、存储、数据库、响应状态码与 HTML 内容组件测试render(...)渲染到真实 DOMElement或createRoot(...)直接控制根节点真实 DOM浏览器/测试容器组件渲染、事件交互、异步更新、清理行为从源码结构看这种分层对应着 Remix 对运行时环境的明确划分packages/fetch-router提供纯 fetch 风格的路由器packages/ui提供独立的虚拟 DOM 渲染运行时packages/test提供统一的测试执行器。理解这两种形态的分工是选择测试层级的起点。服务端 / 路由器测试把路由器当作纯函数核心心智模型在服务端测试中把路由器当作一个纯函数看待(Request) PromiseResponse每个测试或每个套件都新建一个应用路由器实例这样中间件状态——session、内存存储、数据库——彼此完全隔离互不污染。下面的例子来自demos/bookstore的真实测试模式import * as assert from remix/assert import { describe, it } from remix/test import { createBookstoreRouter } from ../app/router.ts import { routes } from ../app/routes.ts describe(home, () { it(responds 200 with the home page, async () { let router createBookstoreRouter() let response await router.fetch(new Request(http://localhost routes.home.href())) assert.equal(response.status, 200) assert.match(await response.text(), /Welcome to the Bookstore/) }) })要点用routes.name.href(...)构建 URL而不是手写字符串。这样测试 URL 始终与路由定义保持同步路由模式一旦变化测试会立即暴露问题。断言先看状态码再看响应体内容assert.equal(response.status, 200)校验 HTTP 语义assert.match(await response.text(), ...)校验页面实际内容。对于 404 等负向用例同样走router.fetch断言状态码即可见 demos/bookstore/app/router.test.ts 中的it(returns 404 for unknown routes)。模拟 session内存存储 测试 cookie需要已知 session 的测试在构造路由器时注入createMemorySessionStorage()与测试用 cookieimport { createMemorySessionStorage } from remix/session-storage/memory import { createCookie } from remix/cookie let router createBookstoreRouter({ sessionCookie: createCookie(session, { secrets: [test] }), sessionStorage: createMemorySessionStorage(), })createCookie来自 packages/cookie/src/index.tssecrets用于签名/校验 cookie 值createMemorySessionStorage来自packages/session包的内存实现packages/session/src/lib/session-storage/memory.ts不落盘、不依赖外部服务天然适合测试。demos/bookstore/test/helpers.ts给出了更完整的生产级测试装配除了内存 session还额外配置了httpOnly: true、sameSite: Lax、maxAge、path等 cookie 选项并封装了getSessionCookie(response)、requestWithSession(url, sessionCookie, init)、login(router, email, password)等辅助函数用于从Set-Cookie头中提取 session cookie、给请求附加Cookie头、以及通过真实登录流程拿到已认证的请求。这些模式可以直接复用到你的会话/认证测试中。表单式 POST附加 FormData body对于表单提交类行为在Request上附加FormDatabody 即可let response await router.fetch(http://localhost/login, { method: POST, body: new URLSearchParams({ email, password }), redirect: manual, })demos/bookstore/test/helpers.ts中的login函数即采用此方式走通真实登录链路redirect: manual让重定向响应原样返回便于从响应头中读取会话 cookie。需要真实网络边界时createTestServer当被测行为依赖真实 HTTP origin、重定向、流式响应、cookie 跨网络边界传播或需要浏览器风格fetch时用remix/node-fetch-server/test提供的createTestServer起一个真实 HTTP 服务import { createTestServer } from remix/node-fetch-server/test let server await createTestServer((request) router.fetch(request)) try { let response await fetch(new URL(routes.home.href(), server.baseUrl)) assert.equal(response.status, 200) } finally { await server.close() }从源码看packages/node-fetch-server/src/lib/test-server.tscreateTestServer内部创建http.Server监听127.0.0.1的随机端口把 fetch 风格的 handler 包装成 request listener返回带baseUrl和close()的TestServerbaseUrl形如http://127.0.0.1:54321把路由 href 拼到它后面即可得到完整 URLclose()会等待所有在途连接关闭后再 resolve保证测试进程干净退出与 Playwright 配合时可以把它和测试运行器提供的 serve 能力配对从Page层面驱动真实浏览器。测试运行器配置测试发现与覆盖率在remix.json的test段配置也可用 CLI 标志覆盖{ $schema: ./node_modules/remix/schema/remix.json, test: { files: [**/*.test{,.e2e}.{ts,tsx}], e2eFiles: [**/*.test.e2e.{ts,tsx}], exclude: [node_modules/**], coverage: { dir: .coverage, include: [app/**/*.{ts,tsx}], exclude: [app/**/*.test.{ts,tsx}], statements: 80, lines: 80, branches: 70, functions: 80 } } }字段说明files所有测试文件的 glob 模式**/*.test{,.e2e}.{ts,tsx}同时匹配*.test.ts、*.test.tsx、*.test.e2e.ts、*.test.e2e.tsxe2eFilesE2E 测试文件的 glob 子集exclude从发现结果中排除的路径。当发现规则会进入构建产物、符号链接的 workspace 或其他不该产生测试的目录时用它排除coverage.dir覆盖率报告输出目录默认.coveragecoverage.include/coverage.exclude参与覆盖率统计的文件范围coverage.statements / lines / branches / functions四个维度的最低覆盖率百分比未达标时remix test会失败。从 packages/test/src/lib/config.ts 的实现看默认值分别为coverage.dir为.coverage、glob.exclude为[node_modules/**]并支持glob.test、glob.browser、glob.e2e、glob.exclude这组更细粒度的 CLI 标志--glob.test等来精确控制发现范围覆盖率配置还区分「布尔开关」与「配置对象」两种形态——传配置对象即默认启用覆盖率inherit则只继承设置而不强制启用。常用 CLI 操作remix test --coverage按默认配置启用覆盖率可通过remix.json覆盖各项阈值与目录。与测试发现直接相关的相邻命令还有remix routes查看路由表用于核对routes.name.href()对应的实际 URL、remix doctor诊断项目配置与依赖健康度、remix version确认当前 Remix 版本判断测试行为与版本是否匹配。组件测试渲染进真实 DOM大多数组件测试使用remix/ui/test的render(...)。它创建真实 DOM 容器、立即冲刷flush初始渲染并返回act(...)包装器让你在交互之后冲刷待处理的更新再做断言。需要显式控制根节点渲染、冲刷与销毁时直接使用remix/ui的createRoot(container)。基本模式import * as assert from remix/assert import { render } from remix/ui/test let result render(Counter /) let button result.$(button)! await result.act(() button.click()) assert.match(result.container.textContent ?? , /1/) result.cleanup()从 packages/ui/src/runtime/render.ts 的实现看render会新建一个div挂到document.body也可通过opts.container指定挂载元素用createRoot(container)创建虚拟根root.render(node)渲染后立即root.flush()完成首屏输出。返回的RenderResult提供container组件挂载的 DOM 元素$/$$querySelector/querySelectorAll的简写用于定位节点act(fn)执行fn后冲刷所有待处理的组件更新保证断言前 DOM 已经反映最新状态cleanup()销毁根并移除容器可传给测试框架的 after 钩子做自动清理root底层的VirtualRoot需要访问调度器或派发生命周期事件时使用。为什么要 act / flush初始渲染之后确保事件监听器已挂载、DOM 已可交互交互之后应用事件触发的handle.update()产生的更新异步工作 resolve 之后应用queueTask(...)回调 resolve 后产生的更新。一句话任何可能产生组件更新的时刻之后都要通过act冲刷否则断言看到的可能还是旧 DOM。异步操作对于在queueTask中做异步操作的组件每个异步步骤之后都要actlet result render(AsyncLoader /) assert.equal(result.container.textContent, Loading...) await waitForFetch() await result.act(() {}) assert.equal(result.container.textContent, Expected data)先断言初始的加载态Loading...等待异步 fetch 完成再act冲刷最后断言数据态。组件移除用result.cleanup()或root.dispose()移除组件树并验证清理行为let result render(MyComponent /) assert.ok(result.$(.content)) result.cleanup() assert.throws(() result.$(.content), /cleaned up/)清理之后container与root的 getter 会抛出「已被清理」的错误见 packages/ui/src/runtime/render.ts 中对已清理状态的保护因此用assert.throws断言清理生效同时可借此验证组件的卸载逻辑没有内存泄漏或悬空引用。组件测试的通用准则优先用真实 DOM 交互而不是 mock 框架行为——真实点击、真实输入测试的才是用户会遇到的路径避免断言纯实现标记除非它是唯一稳定的同步点一个代表性流程胜过在大量路径上重复同一断言——聚焦「一个行为、一条证明路径」。仓库中的实战佐证路由测试的真实样板demos/bookstore/app/router.test.ts 展示了describe/itrouter.fetchassert.equal的最小闭环会话与登录测试装配demos/bookstore/test/helpers.ts 覆盖了内存 session、测试 cookie、登录获取会话 cookie、带会话请求的完整辅助函数集测试运行器与覆盖率实现packages/test/src/lib/config.ts发现与覆盖率默认值、CLI 标志、packages/test/src/lib/coverage.ts覆盖率计算与报告组件渲染测试基础设施packages/ui/src/runtime/render.tsrender的容器/根/act/cleanup 语义、packages/ui/src/test.tsrender的导出入口真实 HTTP 测试服务packages/node-fetch-server/src/lib/test-server.ts。如何选择测试层级场景推荐层级原因路由分发、状态码、重定向服务端/路由器测试快、无浏览器直接断言Responsesession、cookie、存储、数据库隔离服务端/路由器测试每个测试新建路由器即可完全隔离需要真实 HTTP origin、流式、跨网络 cookiecreateTestServer真实网络边界行为只能在真实服务上验证组件渲染、事件交互、异步更新、卸载清理组件测试render/createRoot需要真实 DOM 与调度冲刷端到端用户流程E2E*.test.e2e.*需要浏览器与完整服务链路选择原则很简单能被纯函数式router.fetch验证的行为优先用服务端测试需要真实 DOM 的行为用组件测试只有必须验证跨进程/跨网络完整链路时才上 E2E。【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表