尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Sanity 仓库 E2E 测试架构实践:E2E、组件与 API 测试的选型与分层
Sanity 仓库 E2E 测试架构实践E2E、组件与 API 测试的选型与分层【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity本文聚焦于 Sanity 仓库GitHub 推荐项目精选 / sa / sanity中.agents/skills/playwright-best-practices/architecture/test-architecture.md所定义的核心方法论如何在一个真实的前端项目中为每个特性选择最便宜且能给出足够信心的测试类型。我们会把文档中的决策矩阵、API 测试、组件测试、E2E 测试与分层策略完整落地并结合仓库中真实的e2e/测试套件、e2e/studio-test.ts自定义 fixtures 与e2e/playwright.config.ts配置展示这些原则在生产级项目中的实际形态。读完本文你将能独立完成一次按层选型 — 编写 API 测试 — 编写组件测试 — 编写 E2E 测试 — 用分层策略组织套件的完整实践。测试类型决策矩阵先回答哪个测试最便宜且给信心测试类型的选型核心只有一句话哪个测试能最便宜地给出对这个功能的信心决策顺序永远是先 API、再组件、最后 E2E——能用 API 测的绝不开浏览器能用组件测的绝不端到端。原文档给出的决策矩阵是选型的第一步我们完整复刻如下场景推荐类型理由登录 / 认证流程E2E跨页面、Cookie、重定向、会话状态表单提交组件隔离的校验逻辑、错误状态CRUD 操作API数据完整性比 UI 更重要带结果 UI 的搜索组件 APIAPI 负责查询逻辑组件负责渲染跨页面导航E2E路由、历史记录、深链接API 错误处理API状态码、错误结构、边界情况UI 错误反馈组件Toast、Banner、行内错误渲染可访问性组件逐组件的 ARIA 角色、键盘导航响应式布局组件无需整个应用即可验证视口渲染API 契约校验API响应结构、Header、认证WebSocket / 实时E2E需要完整浏览器环境支付 / 结账E2E多步骤、第三方 iframe引导向导OnboardingE2E多步骤、状态跨页面持久化组件控件行为组件Toggle、手风琴、日期选择器、模态框权限 / 授权API基于角色的访问是后端逻辑使用这张表时的关键判别维度有三个状态归属状态存在于服务端会话、权限、数据完整性→ API状态存在于组件内部校验、展开/收起→ 组件。是否涉及多页面跨页面导航、Cookie、重定向 → 只有 E2E 能覆盖。成本约束API 测试毫秒级且无浏览器组件测试有浏览器但无服务端E2E 最贵最慢只配给关键路径。API 测试验证数据与契约不碰浏览器适合与不适合的场景API 测试的理想场景CRUD 操作create / read / update / delete输入校验与错误响应400、422权限与授权检查数据完整性与业务规则API 契约验证通过 UI 复现成本过高的边界情况为 E2E 测试准备/清理测试数据API 测试应避免的场景测试错误如何展示给用户浏览器特有行为Cookie、重定向视觉布局或响应式设计需要 JavaScript 执行或 DOM 交互的流程第三方 iframe 交互实战示例带认证的商品 API 测试原文档给出了一套完整的商品 API 测试覆盖认证 token 获取、成功创建、重复 SKU 冲突、缺失必填字段、角色权限与分页import {test, expect} from playwright/test test.describe(Products API, () { let token: string test.beforeAll(async ({request}) { const res await request.post(/api/auth/token, { data: {email: managershop.io, password: mgr-secret}, }) token (await res.json()).accessToken }) test(creates product with valid payload, async ({request}) { const res await request.post(/api/products, { headers: {Authorization: Bearer ${token}}, data: {name: Widget Pro, sku: WGT-100, price: 29.99}, }) expect(res.status()).toBe(201) const product await res.json() expect(product).toMatchObject({name: Widget Pro, sku: WGT-100}) expect(product).toHaveProperty(id) }) test(rejects duplicate SKU with 409, async ({request}) { const res await request.post(/api/products, { headers: {Authorization: Bearer ${token}}, data: {name: Duplicate, sku: WGT-100, price: 19.99}, }) expect(res.status()).toBe(409) expect((await res.json()).message).toContain(already exists) }) test(returns 422 for missing required fields, async ({request}) { const res await request.post(/api/products, { headers: {Authorization: Bearer ${token}}, data: {name: Incomplete}, }) expect(res.status()).toBe(422) const err await res.json() expect(err.errors).toContainEqual(expect.objectContaining({field: sku})) }) test(staff role cannot delete products, async ({request}) { const staffLogin await request.post(/api/auth/token, { data: {email: staffshop.io, password: staff-pass}, }) const staffToken (await staffLogin.json()).accessToken const res await request.delete(/api/products/123, { headers: {Authorization: Bearer ${staffToken}}, }) expect(res.status()).toBe(403) }) test(lists products with pagination, async ({request}) { const res await request.get(/api/products, { headers: {Authorization: Bearer ${token}}, params: {page: 1, limit: 20}, }) expect(res.status()).toBe(200) const body await res.json() expect(body.items).toBeInstanceOf(Array) expect(body.items.length).toBeLessThanOrEqual(20) expect(body).toHaveProperty(totalCount) }) })这段代码演示了 API 测试的四个关键习惯beforeAll只做一次认证token 在 describe 块内共享先断言状态码再断言 bodyexpect(res.status()).toBe(409)先于消息断言避免500 兜底 body 蒙混过关错误场景与成功场景一样多409 冲突、422 校验、403 权限一个不少不硬编码 ID需要 ID 时通过创建接口的返回值获取防止数据库重置后测试集体失效。Sanity 仓库的 e2e/tests/inputs/text.spec.ts 中可以看到这种API 校验数据、E2E 校验 UI的混合用法测试通过sanityClient.getDocument()读取草稿文档的远端值再用expect.poll(getRemoteValue, ...)轮询断言浏览器里的输入最终确实被持久化到 Sanity API——UI 操作由 E2E 负责数据正确性由 API 客户端校验两个层各司其职。组件测试隔离验证 UI 逻辑适合与不适合的场景组件测试的理想场景表单校验必填字段、格式规则、错误消息交互控件模态框、下拉、手风琴、日期选择器条件渲染显示/隐藏、加载态、空态逐组件可访问性ARIA 属性、键盘导航不同视口下的响应式布局视觉状态hover、focus、disabled、selected组件测试应避免的场景页面间的路由或导航需要真实 Cookie、会话或服务端状态的流程数据持久化或 API 契约校验第三方 iframe 交互任何需要多页面或多个浏览器上下文的内容实战示例ContactForm 组件测试组件测试通过playwright/experimental-ct-react的mount直接挂载组件无需启动整个应用import { test, expect } from playwright/experimental-ct-react; import { ContactForm } from ../src/components/ContactForm; test.describe(ContactForm component, () { test(displays validation errors on empty submit, async ({ mount }) { const component await mount(ContactForm onSubmit{() {}} /); await component.getByRole(button, { name: Send message }).click(); await expect(component.getByText(Name is required)).toBeVisible(); await expect(component.getByText(Email is required)).toBeVisible(); }); test(rejects malformed email, async ({ mount }) { const component await mount(ContactForm onSubmit{() {}} /); await component.getByLabel(Name).fill(Alex); await component.getByLabel(Email).fill(invalid-email); await component.getByLabel(Message).fill(Hello); await component.getByRole(button, { name: Send message }).click(); await expect(component.getByText(Enter a valid email)).toBeVisible(); }); test(invokes onSubmit with form data, async ({ mount }) { const submissions: Array{ name: string; email: string; message: string } []; const component await mount( ContactForm onSubmit{(data) submissions.push(data)} / ); await component.getByLabel(Name).fill(Alex); await component.getByLabel(Email).fill(alexcompany.org); await component.getByLabel(Message).fill(Inquiry about pricing); await component.getByRole(button, { name: Send message }).click(); expect(submissions).toHaveLength(1); expect(submissions[0]).toEqual({ name: Alex, email: alexcompany.org, message: Inquiry about pricing, }); }); test(disables button during submission, async ({ mount }) { const component await mount( ContactForm onSubmit{() {}} submitting{true} / ); await expect( component.getByRole(button, { name: Sending... }) ).toBeDisabled(); }); test(associates labels with inputs for accessibility, async ({ mount }) { const component await mount(ContactForm onSubmit{() {}} /); await expect( component.getByRole(textbox, { name: Name }) ).toBeVisible(); await expect( component.getByRole(textbox, { name: Email }) ).toBeVisible(); }); });组件测试的价值在于用注入的 props 精确控制状态submitting{true}直接验证提交中的禁用态通过捕获onSubmit回调的入参验证数据以正确结构被提交出去用getByRole(textbox, {name: Name})顺带验证了 label 与输入框的关联——这本身就是一次可访问性检查。所有验证都在组件内部完成不依赖服务端因此毫秒级完成且高度稳定。E2E 测试只测关键路径验证全栈联通适合与不适合的场景E2E 测试的理想场景产生收入的关键用户流程结账、注册认证流程登录、SSO、MFA、密码重置状态跨导航传递的多页面工作流涉及第三方 iframe 的流程支付控件验证整个技术栈的冒烟测试需要多个浏览器上下文的实时协作E2E 测试应避免的场景逐个表单校验排列组合UI 只是薄封装层的 CRUD 操作验证单个组件的状态测试 API 响应结构或错误码每个断点都做响应式布局只影响后端的边界情况实战示例订阅升级流程原文档的 E2E 示例完整展示了用 API 播种数据 用test.step组织关键路径 处理 iframe 支付import {test, expect} from playwright/test test.describe(subscription flow, () { test.beforeEach(async ({page}) { await page.request.post(/api/test/seed-account, { data: {plan: free, email: subscriberdemo.io}, }) await page.goto(/account/upgrade) }) test(upgrades to premium plan, async ({page}) { await test.step(select plan, async () { await expect(page.getByRole(heading, {name: Choose Your Plan})).toBeVisible() await page.getByRole(button, {name: Select Premium}).click() }) await test.step(enter billing details, async () { await page.getByLabel(Cardholder name).fill(Sam Johnson) await page.getByLabel(Billing address).fill(456 Oak Ave) await page.getByLabel(City).fill(Seattle) await page.getByRole(combobox, {name: State}).selectOption(WA) await page.getByLabel(Postal code).fill(98101) await page.getByRole(button, {name: Continue}).click() }) await test.step(complete payment, async () { const paymentFrame page.frameLocator(iframe[titleSecure Payment]) await paymentFrame.getByLabel(Card number).fill(5555555555554444) await paymentFrame.getByLabel(Expiry).fill(09/29) await paymentFrame.getByLabel(CVV).fill(456) await page.getByRole(button, {name: Subscribe now}).click() }) await test.step(verify success, async () { await page.waitForURL(**/account/subscription/success**) await expect(page.getByRole(heading, {name: Welcome to Premium})).toBeVisible() await expect(page.getByText(/Subscription #\d/)).toBeVisible() }) }) })四个要点值得关注beforeEach用 API 播种账户而不是通过 UI 注册——这正是原文档E2E 创建测试数据耗时 90 秒反模式的正面解法test.step()把 5 分钟的大测试拆成可读的步骤失败时报告能精确指向select plan / billing / payment / verify中的某一步iframe 用frameLocator穿透第三方支付 iframe 的交互无法用普通定位器完成断言只落在关键结果URL 跳转、成功标题、订阅编号不重复 API 层已覆盖的细节。分层策略库存管理功能的 60 / 30 / 10 配比原文档的核心洞见是三种测试类型不是竞争关系而是分层协作。以库存管理功能为例API 层约 60% 的测试覆盖所有后端逻辑排列组合便宜且易维护tests/api/inventory.spec.ts - creates item with valid data (201) - rejects duplicate SKU (409) - rejects invalid quantity format (422) - rejects missing required fields (422) - warehouse-staff cannot delete items (403) - unauthenticated request returns 401 - lists items with pagination - filters items by category - updates item stock level - archives an item - prevents archiving items with pending orders组件层约 30% 的测试覆盖所有视觉状态与交互tests/components/InventoryForm.spec.tsx - shows validation errors on empty submit - shows inline error for invalid SKU format - disables submit while saving - calls onSubmit with form data - resets form after successful save tests/components/InventoryTable.spec.tsx - renders item rows from props - shows empty state when no items - handles archive confirmation modal - sorts by column header click - shows stock level badges with correct colorsE2E 层约 10% 的测试只覆盖证明全栈联通的少数关键路径tests/e2e/inventory.spec.ts - manager creates item and sees it in list - manager updates item stock level - warehouse-staff cannot access admin settings执行画像为什么这个配比最优原文档给出了一组量化的执行数据11 个 API 测试— 总计约 2 秒无浏览器10 个组件测试— 总计约 5 秒真实浏览器但无服务端3 个 E2E 测试— 总计约 15 秒全栈合计 24 个测试、约 22 秒。API 测试捕获大部分回归组件测试捕获 UI 缺陷E2E 测试证明接线正确。这里有一个极有价值的诊断推理如果 E2E 失败而 API 与组件测试都通过问题几乎必然出在集成层——路由、状态管理或 API 客户端而不是业务逻辑或 UI 渲染本身。常见反模式与正确做法原文档用一张对照表总结了最常见的选型错误反模式问题正确做法每个校验规则都写 E2E30 秒的浏览器测试API 200ms 就能覆盖校验用 API 测试错误展示用一个组件测试没有 API 测试、全是 E2E套件慢、UI 时序导致 flaky、难以诊断API 测数据/逻辑E2E 只测关键路径组件测试 mock 一切mock 漂移导致测试通过但应用是坏的只 mock 外部边界API 测试验证真实契约API、组件、E2E 三层断言相同内容三倍维护成本每层只测它独有的东西E2E 通过 UI 创建测试数据2 分钟的测试有 90 秒在准备数据在beforeEach里用 API 播种只测真实流程测试第三方行为在测 Stripe 验证卡片那是 Stripe 的职责mock Stripe信任其契约跳过 API 层分不清 bug 在前端还是后端API 测试隔离后端组件测试隔离前端一个巨型 E2E 覆盖整个功能5 分钟测试挂了却不知道挂在哪每个关键路径一个聚焦 E2E配合test.step()这套反模式表可以直接当作测试评审的 checklist 使用。仓库佐证Sanity 的 e2e 套件如何落地这些原则Sanity 仓库的 e2e/ 目录是这些方法论的真实落地案例几个关键文件分别对应上文的原则1. 自定义 fixtures 承载资源生命周期组件/API 层的基建e2e/studio-test.ts 通过test.extend()提供了一组 Sanity 特有的 fixturessanityClient一个指向测试数据集、带 token 的sanity/client实例专用于在测试中断言远端文档值——这是API 校验数据原则的直接体现见 e2e/tests/inputs/text.spec.ts 中sanityClient.getDocument()与expect.poll的配合createDraftDocument在导航路径上创建带唯一 ID 的草稿文档并等待表单进入可编辑态——把测试数据准备从测试体抽离成可复用 fixture_testContext自动跟踪所有创建的文档 ID在测试结束时通过sanityClient.delete({query: *[_id in $ids]})批量清理——即使测试失败teardown 也会执行这正是 fixtures 相对手写 setup 的核心优势_failureDiagnostics{auto: true}失败时自动附加 Studio 诊断报告帮助区分测试问题与Sanity API 行为问题。这印证了原文档姊妹篇 pom-vs-fixtures.md 的判断凡是需要 setup 且需要 teardown的资源一律用自定义 fixture。2. 用page.route在 E2E 中做精准 mock网络拦截e2e/studio-test.ts 中有一条page.route(**/journey/trial**, (route) route.fulfill({...}))拦截免费试用弹窗 API 并返回null防止弹窗遮挡点击。这是只 mock 外部边界、不 mock 自己的前后端通信的精确实践——它只屏蔽一个与核心功能无关的营销弹窗其余所有 Sanity API 请求都走真实 staging。3. 全栈 E2E 的断言分层e2e/tests/document-actions/publish.spec.ts 展示了 E2E 层只断言关键结果的原则发布流程只断言文档面板标题、状态栏的 published 状态、以及平台相关的快捷键提示CtrlOptionP/CtrlAltP具体的数据校验交给 API 层与组件层。4. 真实配置多浏览器项目 storageState 登录态e2e/playwright.config.ts 提供了生产级配置样本多浏览器项目chromium与firefox两个项目macOS 上额外追加webkitDesktop Safari调试时可用SANITY_E2E_DEBUG环境变量收敛到只跑 chromiume2e/playwright.config.tsstorageState预置登录态通过 localStorage 注入__studio_auth_token_${PROJECT_ID}大多数测试无需走登录 UI直接以已认证状态启动——对应 authentication.md 中用storageState跳过登录的模式失败诊断trace: on-first-retry、video: retain-on-failure、retries: 2在 CI 中保留完整故障现场webServer按环境切换CI 用pnpm start生产构建本地用pnpm dev端口 3339reuseExistingServer: !CIglobalSetup预热e2e/globalSetup.ts 在套件启动前先打开一次页面并等待users/me响应确保代码分割后的 JS bundle 已编译完成避免每个测试套件各自承担首次请求慢的初始化惩罚。5. 目录结构即分层结构Sanity 的 e2e/tests/ 目录按功能域组织auth/、desk/、document-actions/、inputs/、navbar/、pte/、releases/、structure/、tasks/等与 test-suite-structure.md 中按功能拆分目录、一个功能一个测试文件的原则一致。结语把分层变成团队的默认动作回到原文档开篇的那个问题哪个测试最便宜且能给信心——正确的答案顺序永远是能用 API 测的不开浏览器能用组件测的不做全栈E2E 留给关键路径。Sanity 仓库的 e2e 套件用真实代码证明了这套方法论的可行性自定义 fixtures 管好资源生命周期、page.route只在边界处 mock、storageState 省去重复登录、globalSetup 消除初始化抖动最终得到的是一个API 层兜住逻辑、组件层兜住 UI、E2E 层证明接线的稳定套件。无论你维护的是电商、SaaS 还是像 Sanity 这样复杂的内容工作室这套决策矩阵 分层配比 反模式清单都可以直接复制到下一个功能里。关联阅读test-architecture.md 所在技能包入口 — 全部 Playwright 最佳实践技能文档test-suite-structure.md — 文件结构与命名规范api-testing.md — PlaywrightrequestAPI 的 HTTP 测试进阶component-testing.md — 组件测试完整配置authentication.md — 基于storageState的认证流程模式when-to-mock.md — 何时 mock、何时打真实服务pom-vs-fixtures.md — 共享测试逻辑的组织方式Sanity 仓库 e2e 配置 — 生产级 Playwright 配置实例【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

draw.io 桌面版安装:4种方式,离线免费画图

draw.io 桌面版安装:4种方式,离线免费画图

draw.io 桌面版安装:4种方式,离线免费画图 【免费下载链接】drawio-desktop Official electron build of draw.io 项目地址: https://gitcode.com/GitHub_Trending/dr/drawio-desktop 这是 draw.io 桌面版的安装与上手指南,教你把这款…

📅 2026/9/17 23:44:08
Flower 1.11.0 版本深度解析:FAB 动态代码分发、ClientApp 隔离执行与企业级 Docker 部署

Flower 1.11.0 版本深度解析:FAB 动态代码分发、ClientApp 隔离执行与企业级 Docker 部署

Flower 1.11.0 版本深度解析:FAB 动态代码分发、ClientApp 隔离执行与企业级 Docker 部署 【免费下载链接】flower Flower: A Friendly Federated AI Framework 项目地址: https://gitcode.com/GitHub_Trending/flo/flower Flower(A Friendly Fed…

📅 2026/9/17 23:44:08
gRPC 网关部署:REST 可达与 gRPC 故障的分工排查

gRPC 网关部署:REST 可达与 gRPC 故障的分工排查

gRPC 网关部署:REST 可达与 gRPC 故障的分工排查工具地址:https://www.speedce.com 社区论坛:https://bbs.speedce.com 联系:speedceadsgmail.com写在前面 SpeedCE 测 REST/HTTP 可达,gRPC 需要专项工具。 本文是一份围…

📅 2026/9/17 23:39:07
MORE NEWS

更多资讯

📰

Java图形绘制系统:Figure抽象类与多态绘图实践

简介:本资源是西南科技大学《Java程序设计与实践》课程配套实验三的完整报告文档,面向Java初学者及高校计算机类专业学生,聚焦类的继承、抽象类设计、多态实现与GUI事件驱动编程等核心OOP能力训练。实验通过构建Figure抽象父类及RightTriangl…

📰

吃透经典50道SQL练习题:从多表查询到执行计划优化的进阶指南

做SQL练习这件事,我一直有个观点:与其漫无目的地刷一百道碎片题,不如踏踏实实把一套经典题吃透。经典50道SQL练习题就是这样一套值得反复练手的题库,它表面上是50道查询题,实际上把SQL开发中绝大多数核心场景都串了一遍…

📰

HarmonyOS hdc命令行实战:从设备连接到日志抓取的完整指南

搞开发这几年,我养成了一个习惯:不管用什么工具链,第一件事不是翻文档,而是先把它的命令行工具摸一遍。命令行是效率的底线,图形界面再方便,等你要写脚本、做自动化、批量处理的时候,终究还得回…

📰

Keil5安装配置:C51与MDK-ARM双工具链共存、Pack与授权指南

1. 先搞清楚 Keil5 到底是什么:一个外壳,三套编译器1.1 C51、C251、MDK-ARM 其实是三套并行的工具链刚接触 Keil 的人最容易犯的一个认知错误,是把 Keil5 当成"一个软件"。实际情况是:你在官网下载到的那些安装包&#…

📰

嵌入式工程方法论:从点灯到工业级产品开发全链路

1. 这套200集嵌入式自学教程到底在解决什么问题?“自学嵌入式能救一个是一个”——这句话不是营销话术,而是我带过37个零基础转行学员、参与过6个工业级嵌入式产品从立项到量产全过程后,最真实的切肤之痛。过去三年,我每年都会收到…

📰

Session与JWT鉴权机制深度对比与实践指南

1. 鉴权机制的选择困境现代Web开发中最让人纠结的技术决策之一,就是如何选择用户身份验证方案。我经历过从传统Session到JWT的完整迁移过程,也踩过不少坑。这两种机制看似简单,但在实际业务场景中的表现差异巨大。Session-Cookie就像老式的会…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬