Refine v5 测试指南:理解可测试性设计,用 Cypress 为内部工具编写端到端测试
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
Refine 官方文档将测试策略划分为"单元测试"与"端到端测试"两个层面:框架自身的组件与 hooks 已由维护者完成单元测试,因此推荐开发者把精力集中在应用级别的端到端测试上。本文将基于 Refine v5 官方测试指南(documentation/docs/further-readings/testing.md),结合仓库中真实的 Cypress 测试基础设施(cypress/ 目录),系统讲解 Refine 的可测试性设计理念、单元测试与 E2E 测试的边界划分,并给出可直接复用的 Cypress 测试编写范式。
Refine 的测试设计哲学:小代码块与独立可测
Refine 的组件和 hooks 是由"小块代码"(small pieces of code)构成的。每个组件或 hook 都被刻意设计为可测试的,并且彼此独立工作——这意味着:
- 单个 hook 不依赖页面上下文即可被单独调用和断言;
- 单个组件不依赖外部全局状态即可渲染和交互;
- 数据 provider、auth provider、路由等能力通过明确的接口注入,天然易于在测试中替换或拦截。
这种"小而独立"的设计,正是官方测试策略的底层支撑:既然框架内部每一个单元都已被验证为可独立工作,开发者就没有必要再为框架本身编写重复的单元测试。
从仓库源码也能印证这一点:框架核心包 packages/core 与各 UI 集成包(packages/antd、packages/mui、packages/chakra-ui、packages/mantine 等)均自带测试文件与快照(如 packages/inferencer 下的*.snap),说明框架层测试由维护者在包内完成,与官方文档的表述一致。
单元测试:写什么,不写什么
官方指南给出的单元测试边界非常明确:
你不需要为 Refine 编写单元测试,因为 Refine 已经被其维护者测试过了。但你可以在自己的代码中编写单元测试(helper、definitions 等)。
翻译成实操原则就是:
| 层面 | 是否建议写单元测试 | 说明 |
|---|---|---|
| Refine 组件 / hooks 的使用方式 | 不需要 | 框架层已被维护者测试覆盖,重复测试徒增维护成本 |
| 业务 helper、工具函数、类型定义 | 建议 | 这是你完全自有的逻辑,值得用最小成本锁定行为 |
| 复杂的数据转换、权限判断逻辑 | 建议 | 纯函数最容易测试,收益最高 |
| 页面 / 组件 / 用户流程 | 不建议用单元测试 | 交给端到端测试,见下文 |
换句话说,单元测试只服务于"你自己的纯逻辑",而所有涉及渲染、路由跳转、表单提交、通知反馈的用户可见行为,都应交给端到端测试去覆盖。
端到端测试:官方强烈推荐的方向
文档明确写道:
我们强烈建议你为自己的应用程序编写端到端测试。Refine 以 Cypress 框架作为示例,你可以自由选择任何你喜欢的测试框架。
内部工具、管理面板与 B2B 应用的价值在于"整个流程跑得通":登录 → 列表 → 创建 → 编辑 → 删除 → 反馈通知。这类跨组件、跨路由、跨请求的完整链路,只有端到端测试才能真实还原。
值得注意的是,文档中原本指向的示例with-cypress如今在仓库中已是一个"已删除"的占位说明:examples/with-cypress/README.md 明确写道:
This example has been deleted because many examples below have been support Cypress test.
也就是说,Cypress 测试能力已经被下沉到了大量示例仓库本身,读者可以直接在 examples/base-antd、examples/base-chakra-ui、examples/base-mantine 等示例上运行测试,无需再依赖独立的 with-cypress 示例。
仓库中的 Cypress 测试基础设施解剖
仓库根目录的 cypress/ 是整个项目的 E2E 测试中枢,其结构清晰、分层合理,是学习 Refine 官方测试范式的第一手资料。
1. 全局配置:一次看懂官方如何调教 Cypress
cypress/cypress.config.ts 是官方测试的全局配置,关键项及含义如下:
| 配置项 | 值 | 作用 |
|---|---|---|
projectId | "sq5j3e" | 关联 Cypress Cloud 的仪表盘项目 ID |
retries.runMode | 3 | 命令行运行时失败最多重试 3 次,缓解偶发不稳定 |
chromeWebSecurity | false | 关闭跨域安全限制,允许跨域请求(配合 API 拦截) |
experimentalMemoryManagement | true | 开启实验性内存管理,适合跑大量用例的长任务 |
numTestsKeptInMemory | 1 | 每个用例在内存中保留的 DOM 快照数最小化,节省内存 |
viewportWidth/viewportHeight | 1920/1080 | 统一桌面视口,保证断言可复现 |
e2e.baseUrl | "http://localhost:5173" | 本地开发服务器地址(Vite 默认端口) |
setupNodeEvents中还做了一件事:把浏览器过滤为仅保留 chromium 系且排除 electron,即config.browsers.filter((b) => b.family === "chromium" && b.name !== "electron"),确保测试始终跑在 Chrome/Edge 等标准 Chromium 浏览器上。
2. 测试规格的组织方式:一个示例目录一套用例
cypress/e2e/ 下按"示例名 + 测试文件"的方式组织,例如:
- cypress/e2e/base-antd/all.cy.ts —— base-antd 示例的完整 CRUD 流程
- cypress/e2e/table-antd-advanced/ —— 拆分为
categories.cy.ts与posts.cy.ts两个资源 auth-*、form-*、inferencer-*、with-nextjs、with-remix-*等 80+ 个规格文件
以 base-antd 的 all.cy.ts 为例,它用一个describe覆盖了资源的五个核心动作:
describe("base-antd", () => { beforeEach(() => { cy.clearAllCookies(); cy.clearAllLocalStorage(); cy.clearAllSessionStorage(); cy.visit("/"); }); it("should list resource", () => { cy.resourceList(); }); it("should create resource", () => { cy.resourceCreate({ ui: "antd" }); }); it("should edit resource", () => { cy.resourceEdit({ ui: "antd" }); }); it("should show resource", () => { cy.resourceShow(); }); it("should delete resource", () => { cy.resourceDelete({ ui: "antd" }); }); });注意it内部几乎没有选择器,全部委托给cy.resourceList()、cy.resourceCreate()这类自定义命令——这正是官方把"操作逻辑"与"测试声明"解耦的做法。
3. 自定义命令:跨 UI 框架的适配层
不同 UI 集成(antd、chakra-ui、mantine、material-ui)的 DOM 结构完全不同,官方通过两层命令把差异封装起来:
第一层:语义化资源操作命令(cypress/support/commands/resource.ts)。它只声明"创建资源"这个意图,内部根据ui参数分发到对应的 UI 适配命令,例如:
const assertNotification = (ui: UITypes) => { switch (ui) { case "antd": return cy.getAntdNotification().should("contain", "Success"); case "chakra-ui": return cy.getChakraUINotification().should("contain", "Success"); case "mantine": return cy.getMantineNotification().should("contain", "Success"); case "material-ui": return cy.getMaterialUINotification().should("contain", "Success"); } };第二层:具体 UI 框架的选择器命令。例如 cypress/support/commands/refine/index.ts 通过 Refine 自带的稳定 class 定位按钮:
export const getSaveButton = () => cy.get(".refine-save-button"); export const getCreateButton = () => cy.get(".refine-create-button"); export const getDeleteButton = () => cy.get(".refine-delete-button"); export const getEditButton = () => cy.get(".refine-edit-button"); export const getShowButton = () => cy.get(".refine-show-button"); export const getPageHeaderTitle = () => cy.get(".refine-pageHeader-title");而 cypress/support/commands/antd/index.ts 则封装了 antd 特有的交互,如选择下拉、日期选择、Popconfirm 删除确认、表格排序器等:
export const setAntdSelect = ({ id, value }: ISetAntdSelectParams) => { return cy .get(`#${id}`) .click({ force: true }) .get(`.ant-select-item[title="${value}"]`) .click({ force: true }) .get(`#${id}`) .blur(); };所有命令统一在 cypress/support/e2e.ts 中通过Cypress.Commands.add(...)注册,并集中调整了超时参数(defaultCommandTimeout与requestTimeout均为 20000ms)。
4. 网络拦截与 fixture:不依赖真实后端的 E2E
这是官方测试范式中最有价值的部分之一:测试完全不依赖真实 API,而是通过cy.intercept把api.fake-rest.refine.dev上的请求全部拦下并用本地 fixture 应答。
cypress/support/commands/intercepts/api-fake-rest.ts 为posts、categories、blog_posts三个资源定义了 GET/POST/PATCH/DELETE 全套拦截命令。以 GET 单个 post 为例:
Cypress.Commands.add("interceptGETPost", () => { return cy .fixture("posts") .then((posts) => { return cy.intercept( { method: "GET", hostname: hostname, // api.fake-rest.refine.dev pathname: "/posts/*", }, (req) => { const id = getIdFromURL(req.url); const post = posts.find((post) => post.id === id); if (!post) { req.reply(404, {}); return; } req.reply(post); }, ); }) .as("getPost"); });POST/PATCH 则模拟了真实后端行为——把请求体与 fixture 合并并分配新 ID:
Cypress.Commands.add("interceptPOSTPost", () => { return cy.fixture("posts").then((posts) => cy .intercept( { method: "POST", hostname: hostname, pathname: "/posts" }, (req) => { const merged = Object.assign({}, req.body, { id: posts.length + 1 }); return req.reply(merged); }, ) .as("postPost"), ); });这些拦截命令在 cypress/support/e2e.ts 的全局beforeEach中被统一注册(同时把 telemetry 请求拦截掉,避免污染统计数据):
beforeEach(() => { cy.intercept("https://telemetry.refine.dev/**", { ... }).as("telemetry"); cy.interceptGETPosts(); cy.interceptGETPost(); cy.interceptPOSTPost(); cy.interceptPATCHPost(); cy.interceptDELETEPost(); cy.interceptGETBlogPosts(); cy.interceptGETBlogPost(); cy.interceptPOSTBlogPost(); cy.interceptPATCHBlogPost(); cy.interceptDELETEBlogPost(); cy.interceptGETCategories(); cy.interceptGETCategory(); });除了 fake-rest 外,cypress/support/commands/intercepts/index.ts 还统一引入了 supabase、strapi-v4、hasura 等数据 provider 的拦截实现,让同一套测试代码可以横跨不同后端。
测试数据统一存放在 cypress/fixtures/:posts.json、categories.json、blog-posts.json、mock-post.json、各认证服务的凭据文件等。例如创建资源时表单填充就使用mock-post.json中的固定数据,保证断言结果可预期。
从零开始:写一个 Refine 应用的端到端测试
综合上面拆解的基础设施,在自己的 Refine 项目中落地 E2E 测试,可以按以下步骤:
第一步:安装并配置 Cypress
# 在你的 Refine 项目根目录 npm install -D cypress npx cypress open参照 cypress/cypress.config.ts 配置baseUrl指向本地开发服务器(Refine 示例默认 Vite 端口http://localhost:5173),并按需设置viewportWidth/Height、retries等参数。
第二步:建立 fixtures 与网络拦截
- 把接口返回数据写入
cypress/fixtures/(如posts.json); - 用
cy.intercept按资源路径拦截 GET/POST/PATCH/DELETE 请求,把.as()命名给拦截别名,例如cy.intercept(...).as("getPosts"),后续在测试中用cy.wait("@getPosts")等待请求完成并读取响应。
第三步:封装自定义命令,隐藏 UI 细节
- 参考 cypress/support/commands/refine/index.ts,优先使用 Refine 输出的稳定 class(
refine-save-button、refine-create-button等)定位按钮; - 如果使用 antd 等 UI 库,参考 cypress/support/commands/antd/index.ts 封装下拉、日期、Popconfirm 等专属交互;
- 在
cypress/support/e2e.ts中用Cypress.Commands.add完成注册。
第四步:编写资源级测试
参考 cypress/support/commands/resource.ts 的create实现,一套标准的"创建资源"流程是:
export const create = ({ ui }: IResourceCreateParams) => { cy.getCreateButton().click(); cy.wait("@getCategories"); cy.location("pathname").should("eq", "/posts/create"); cy.assertDocumentTitle("Post", "create"); fillForm(ui); cy.getSaveButton().click(); cy.wait("@postPost").then((interception) => { const response = interception?.response; assertSuccessResponse(response, ui); }); };其中assertSuccessResponse会依次校验:HTTP 状态码为 200、响应包含id与category字段、字段值与表单提交内容一致、出现 "Success" 通知、路由回到/posts。这种"拦截别名 + 响应断言 + 路由断言 + 通知断言"的组合,就是官方推荐的 E2E 断言范式。
第五步:覆盖文档标题等细节
仓库中还提供了文档标题断言命令 cypress/support/commands/document-title-handler.ts(通过cy.assertDocumentTitle("Posts", "list")使用),用于验证 list/create/edit/show 各页面的document.title是否符合 Refine 的标题约定——这也是容易被忽略但很有价值的回归点。
运行与维护建议
- 运行方式:先启动示例应用(例如
npm run dev),再执行npx cypress run(无头模式)或npx cypress open(交互模式);配置中的baseUrl必须与本地开发服务器一致。 - 失败重试:E2E 天然存在偶发不稳定,官方在
cypress.config.ts中设置了retries.runMode: 3,建议在 CI 中对重试次数做同样的配置。 - 保持拦截与 fixture 同步:每当你调整应用的字段或 API 契约,同步更新 cypress/fixtures/ 与拦截命令,避免"测试通过但数据已过期"的假阳性。
- 多框架复用:如果你的产品同时提供 antd / MUI / Chakra UI / Mantine 版本,可以借鉴
resource.ts的ui参数分发模式,一套测试逻辑覆盖多个 UI 适配层。 - 自由选择框架:官方文档强调 Cypress 只是示例,你可以自由选用 Playwright 等任何你熟悉的 E2E 框架,但"拦截 API + fixture 数据 + 语义化命令 + 流程断言"这套方法论是通用的。
总结
Refine v5 的测试策略可以浓缩为三句话:框架层单元测试交给维护者;业务纯逻辑用单元测试锁定;用户可见的完整流程用端到端测试守护。仓库中 cypress/ 目录提供了高质量的可参考实现——从全局配置、跨 UI 的命令适配层,到不依赖后端的拦截与 fixture 体系,再到覆盖 80+ 个示例规格的测试用例,直接对照学习即可在自己的内部工具、管理面板项目中落地同等水平的测试工程。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考