- 前端
- 开发工具
【免费下载链接】dillinger
The last Markdown editor, ever.
本文以 Dillinger(Next.js + React + TypeScript 实现的 Markdown 编辑器)仓库中的真实测试工程为依托,系统讲解测试金字塔、AAA 模式、测试类型选择、单元/集成测试原则、Mock 策略、测试组织、测试数据与反模式等软件测试核心方法论,并逐一对照仓库中的
vitest单元测试、路由集成测试与playwrightE2E 测试给出可复制的落地示例。读完本文,你将掌握一套"既有方法论骨架、又有源码级实证"的测试编写规范,并可直接套用到任意前端项目中。
1. 测试金字塔:让测试套件拥有正确的层级配比
测试金字塔是构建测试策略的起点。它的核心思想是:底层单元测试数量最多、速度最快、执行成本最低;越往上层,测试越少、越慢、越接近真实用户路径。
/\ E2E (Few) / \ Critical flows /----\ / \ Integration (Some) /--------\ API, DB queries / \ /------------\ Unit (Many) Functions, classes- 单元测试(Many):覆盖函数、类等最小逻辑单元,数量最多,执行毫秒级完成。
- 集成测试(Some):覆盖 API、数据库查询等跨模块协作路径,数量适中。
- E2E 测试(Few):只覆盖关键用户流程,数量最少,因为其运行最慢、最脆弱。
Dillinger 仓库正是按这一金字塔组织的:tests/lib/、tests/store/、tests/hooks/、tests/components/下是大量单元测试;tests/routes/下是对 Next.js API Route 的集成测试;tests/e2e/下是少量 Playwright 端到端测试(如 editor.spec.ts)。从目录布局即可看出,"多而快"的测试被放在金字塔底部,这正是金字塔原则在真实项目中的直接体现。
2. AAA 模式:每个测试的固定三段式结构
AAA(Arrange–Act–Assert)是编写清晰测试的统一范式:
| Step | Purpose |
|---|---|
| Arrange | 准备测试数据(Set up test data) |
| Act | 执行被测代码(Execute code under test) |
| Assert | 验证结果(Verify outcome) |
在 store.test.ts 中可以逐行看到 AAA 的完整落地。以hydrate的用例为例:
it("restores documents from localStorage", () => { // Arrange:写入 localStorage 作为测试数据 const doc = createTestDocument({ id: "stored-1", title: "Stored.md" }); localStorage.setItem("files", JSON.stringify([doc])); localStorage.setItem("currentDocument", JSON.stringify(doc)); // Act:执行被测动作 useStore.getState().hydrate(); // Assert:校验副作用结果 const state = useStore.getState(); expect(state.documents).toHaveLength(1); expect(state.documents[0]?.id).toBe("stored-1"); expect(state.currentDocument?.id).toBe("stored-1"); });再如 export-markdown.route.test.ts 中对POST /api/export/markdown的断言:
it("returns 400 when markdown body is missing", async () => { // Arrange const response = await exportMarkdown(buildRequest({ title: "no-body.md" })); // Act(发生在 await 中)之后直接 Assert expect(response.status).toBe(400); expect(json.error).toBe("Markdown content is required"); });坚持 AAA 三段式,能让测试的意图一目了然,任何读者都能快速定位"数据从哪来、测了什么、期望什么"。
3. 测试类型选择:何时用哪种测试
| Type | Best For | Speed |
|---|---|---|
| Unit | Pure functions, logic | Fast(参考阈值 <50ms) |
| Integration | API, DB, services | Medium |
| E2E | Critical user flows | Slow |
Dillinger 的工程配置与之一一对应:
- 单元测试由 Vitest 驱动,配置见 vitest.config.ts:
environment: "jsdom",include: ["tests/**/*.test.ts", "tests/**/*.test.tsx"]。典型如 markdown.test.ts 中renderMarkdown的纯函数渲染断言、store.test.ts 中 Zustand store 的状态迁移断言。 - 集成测试同样基于 Vitest,但通过文件头注释
// @vitest-environment node切换到 Node 环境,直接调用路由处理函数并断言 HTTP 状态码与响应头,见 export-markdown.route.test.ts。 - E2E 测试由 Playwright 驱动,配置见 playwright.config.ts:
testDir: "./tests/e2e",通过webServer自动启动npx next dev -H 127.0.0.1 -p 3005供测试访问(配置注释说明:因生产构建存在既有预渲染错误,E2E 使用 dev 模式)。
选择原则:优先用单元测试覆盖逻辑;跨模块交互交给集成测试;只有核心用户流程(新建、切换、删除文档、Zen 模式、设置持久化等)才动用 E2E,见 editor.spec.ts。
4. 单元测试原则
好的单元测试应具备的五个属性
| Principle | Meaning |
|---|---|
| Fast | 参考阈值 <100ms each |
| Isolated | 无外部依赖(No external deps) |
| Repeatable | 结果始终一致(Same result always) |
| Self-checking | 无需人工验证(No manual verification) |
| Timely | 与代码同步编写(Written with code) |
Dillinger 的单元测试通过两层机制保障这五个属性:
- 隔离环境:vitest.setup.ts 为每个测试文件注入
localStorage、matchMedia与ResizeObserver的内存实现,使组件与 store 测试完全不依赖真实浏览器 API;vitest.config.ts开启restoreMocks: true与clearMocks: true,保证 mock 状态在用例之间自动复位。 - 确定性输入:markdown.test.ts 使用固定 Markdown 字符串断言固定的 HTML 输出,例如
"**bold text**"断言输出<strong>bold text</strong>,并专门用一组"一致性"用例验证renderMarkdown对同一输入多次调用返回相同结果——这正是 Repeatable 原则的测试化表达。
什么该测、什么不该测
| Test | Don't Test |
|---|---|
| Business logic | Framework code |
| Edge cases | Third-party libs |
| Error handling | Simple getters |
仓库中的边界用例很有参考价值:store.test.ts专门验证"localStorage 中 JSON 损坏时hydrate不崩溃、状态保持不变"(捕获SyntaxError并打印Failed to hydrate state:),useGitHub.test.ts(tests/hooks/useGitHub.test.ts)则系统覆盖"网络错误、非 2xx 响应时各 API 方法优雅降级"——这些都是典型的"边界与错误处理"应当测试的场景。
5. 集成测试原则
测什么
| Area | Focus |
|---|---|
| API endpoints | Request/response |
| Database | Queries, transactions |
| External services | Contracts |
Dillinger 的集成测试聚焦 API endpoints:tests/routes/ 下的用例直接构造Request对象调用路由的POST处理器,断言状态码、Content-Disposition文件名(如Draft.md、My_Notes.md)、Content-Type: text/markdown; charset=utf-8、空正文返回 400、非法 JSON 返回 500 等契约行为。路由测试以请求/响应契约为核心,不依赖真实浏览器。
设置与清理
| Phase | Action |
|---|---|
| Before All | 连接资源(Connect resources) |
| Before Each | 重置状态(Reset state) |
| After Each | 清理(Clean up) |
| After All | 断开连接(Disconnect) |
对应实现:
- 单元/组件级:vitest.setup.ts 通过
afterEach统一执行cleanup()(卸载 Testing Library 渲染的 DOM)并localStorage.clear();store.test.ts与settings-modal.test.tsx(tests/components/settings-modal.test.tsx)在beforeEach中调用resetStore()把 Zustand store 恢复到初始快照。 - E2E 级:playwright.config.ts 配置
trace: "on-first-retry"、screenshot: "only-on-failure"、video: "retain-on-failure",失败即自动留痕,便于排查;测试内通过page.addInitScript在页面加载前注入localStorage种子数据,实现"可重复、无脏状态"的用例隔离。
6. Mock 原则
何时 Mock
| Mock | Don't Mock |
|---|---|
| 外部 API(External APIs) | 被测代码本身(The code under test) |
| 数据库(单元测试中) | 简单依赖(Simple dependencies) |
| 时间/随机数(Time/random) | 纯函数(Pure functions) |
| 网络(Network) | 内存存储(In-memory stores) |
Mock 类型
| Type | Use |
|---|---|
| Stub | 返回固定值(Return fixed values) |
| Spy | 记录调用(Track calls) |
| Mock | 设置预期(Set expectations) |
| Fake | 简化实现(Simplified implementation) |
Dillinger 的测试提供了教科书级的 Mock 范例:
- Stub + Spy 结合:useGitHub.test.ts 在
beforeEach中用vi.stubGlobal("fetch", fetchMock)替换全局 fetch,并让fetchMock = vi.fn(() => mockFetchResponse({ connected: false, user: null }))——既能按 URL 返回固定响应(Stub),又能事后断言fetchMock被以哪些参数调用(Spy),例如expect(fetchMock).toHaveBeenCalledWith("/api/github/status")。 - Fake:vitest.setup.ts 用
Map实现了一个完整的localStorageFake,并 stub 掉matchMedia与ResizeObserver,使 jsdom 环境下组件测试可运行。 - 按 URL 分流的响应编排:
useGitHub.test.ts中大量使用fetchMock.mockImplementation((url) => ...),按/api/github/status、/api/github/repos?owner=...、/api/github/branches?...、/api/github/files?...分别返回不同数据,模拟一个完整的仓库浏览状态机。 - 编辑器实例的 Mock:store.test.ts 用
vi.fn()伪造Monaco.editor.IStandaloneCodeEditor(getSelection、executeEdits、focus),从而在不启动 Monaco 的情况下验证insertMarkdownAtCursor会以"dillinger-inline-insert"为资源标识调用executeEdits。
关键边界:只 Mock 外部依赖(网络、全局 API、重型第三方),绝不 Mock 被测代码自身。上述测试全部围绕真实组件、真实 hook、真实 store 逻辑展开,Mock 的只是它们的环境依赖。
7. 测试组织:命名与分组
命名模式
| Pattern | Example |
|---|---|
| Should behavior | "should return error when..." |
| When condition | "when user not found..." |
| Given-when-then | "given X, when Y, then Z" |
分组结构
| Level | Use |
|---|---|
| describe | 分组相关测试(Group related tests) |
| it/test | 单个用例(Individual case) |
| beforeEach | 公共初始化(Common setup) |
Dillinger 的测试命名可以归纳为三种风格,均满足"描述性命名即文档"的要求:
- 行为驱动式:如
"hydrates a default document when storage is empty"、"creates a new imported document without overwriting the current one"(store.test.ts)。 - 能力边界式:如
"fetchBranches handles network error gracefully"、"fetchFileContent returns null for non-ok API response"(useGitHub.test.ts)。 - 功能断言式:如
"renders checked checkboxes"、"applies bootstrap table classes to tables"(markdown.test.ts)。
分组上,store.test.ts 用嵌套describe("createDocument" / "hydrate" / "persist" / "deleteDocument" ...)把同一 action 的多个用例聚在一起;E2E 侧 editor.spec.ts 则用test.describe("Document creation" / "Zen mode" / "Settings modal" ...)按用户流程组织,beforeEach中统一执行种子数据注入。
8. 测试数据策略
三种数据生成方式
| Approach | Use |
|---|---|
| Factories | 生成测试数据(Generate test data) |
| Fixtures | 预定义数据集(Predefined datasets) |
| Builders | 流式对象创建(Fluent object creation) |
Dillinger 仓库同时使用了 Factory 与 Fixture 两种策略,可作为参考样板:
- Factory(工厂函数):store.test.ts 定义
createTestDocument(overrides),以默认值 + 覆盖项的方式快速生成文档对象:
function createTestDocument(overrides: Partial<{ id: string; title: string; body: string; createdAt: string; }> = {}) { return { id: overrides.id ?? "doc-1", title: overrides.title ?? "Test.md", body: overrides.body ?? "# Test", createdAt: overrides.createdAt ?? "2026-03-10T00:00:00.000Z", }; }- Fixture(固定数据集):editor.spec.ts 定义
seededDocument、secondDocument、defaultProfile作为 E2E 种子,并通过seedSingleDocument(page)/seedMultipleDocuments(page)封装page.addInitScript,在页面加载前写入files、currentDocument、profileV3三个 localStorage 键。
数据原则
- 使用贴近真实的数据(如文档标题带
.md后缀、body 为真实 Markdown) - 对非关键字段随机化(仓库中测试因追求确定性而使用固定值,随机化工具如 faker 可按需引入)
- 共享公共 fixtures(
defaultProfile在多文件间复用) - 保持数据最小化(只放被测路径需要的字段)
9. 最佳实践清单
| Practice | Why |
|---|---|
| 每个测试一个核心断言(One assert per test) | 失败原因清晰(Clear failure reason) |
| 测试相互独立(Independent tests) | 无顺序依赖(No order dependency) |
| 测试保持快速(Fast tests) | 可以频繁运行(Run frequently) |
| 描述性命名(Descriptive names) | 自文档化(Self-documenting) |
| 及时清理(Clean up) | 避免副作用(Avoid side effects) |
这些实践在 Dillinger 工程中已被制度化:
- 独立性:
store.test.ts的resetStore()、beforeEach中的localStorage.clear(),加上vitest.config.ts的restoreMocks/clearMocks,共同保证用例之间零状态泄漏。 - 快速:单元与组件测试在 jsdom 中毫秒级完成;
package.json把测试拆分为test:unit(vitest run)与test:e2e,日常开发可只跑前者,避免被 E2E 拖慢节奏。 - 清理:
vitest.setup.ts的afterEach统一执行 Testing Librarycleanup()与存储清空,组件测试无需各自手动卸载。 - 可观测:Playwright 失败自动产出 trace、截图与视频(见 playwright.config.ts),把排查成本压到最低。
10. 反模式:这些做法必须避免
| ❌ Don't | ✅ Do |
|---|---|
| 测试实现细节(Test implementation) | 测试行为(Test behavior) |
| 重复测试代码(Duplicate test code) | 使用工厂(Use factories) |
| 复杂测试搭建(Complex test setup) | 简化或拆分(Simplify or split) |
| 忽略偶发失败(Ignore flaky tests) | 修复根因(Fix root cause) |
| 跳过清理(Skip cleanup) | 重置状态(Reset state) |
仓库中的正面示例恰是这些反模式的镜像:
- 测行为而非实现:markdown.test.ts 只断言渲染产出的 HTML 结构与内容,从不断言 markdown-it 的内部调用;settings-modal.test.tsx 通过
getByRole("switch", ...)与aria-checked断言可访问的行为,而非组件内部 state 变量。 - 用工厂消灭重复:
createTestDocument、seedSingleDocument正是为消除"每个用例手写 localStorage 种子"的重复而存在。 - 直面失败而非忽略:
useGitHub.test.ts对网络错误、非 2xx 响应逐一断言"优雅降级"结果,而不是try/catch吞掉异常;Playwright 的trace: "on-first-retry"则是为偶发失败保留诊断证据、定位根因。 - 主动重置状态:从
beforeEach的resetStore()到afterEach的cleanup(),再到 vitest 配置级的 mock 复位,形成三层清理防线。
11. 在 Dillinger 仓库中运行与验证
仓库在 package.json 中提供了完整的测试命令矩阵:
npm run test:unit # vitest run —— 跑单元 + 路由集成测试 npm run test:watch # vitest —— 监听模式,开发时持续反馈 npm run test:e2e # npm run build && playwright test —— 构建后跑 E2E npm run test:e2e:headed # 以有头浏览器模式运行 E2E,便于观察 npm run test # 依次执行 test:unit 与 test:e2e npm run verify # lint + typecheck + 单元 + E2E 全量门禁其中verify把 lint(next lint)、typecheck(tsc --noEmit)、单元测试与 E2E 串成一条发布前门禁;start:test(next start -H 127.0.0.1 -p 3005)则为手动联调提供与 E2E 一致的端口。E2E 依赖的浏览器与服务器由 playwright.config.ts 管理:webServer自动拉起npx next dev -H 127.0.0.1 -p 3005,reuseExistingServer: !process.env.CI允许本地复用已启动的实例。
12. 测试即文档
Remember:Tests are documentation. If someone can't understand what the code does from the tests, rewrite them.
(测试就是文档。如果别人无法通过测试理解代码在做什么,就重写它们。)
这条原则在 Dillinger 仓库中得到了彻底执行:markdown.test.ts 完整记录了renderMarkdown支持的全部 Markdown 能力(标题、粗斜体、高亮、脚注、上下标、删除线插入、定义列表、缩写、目录、KaTeX 数学公式、行号属性、表格类名、标题锚点);useGitHub.test.ts 完整刻画了 GitHub 集成的状态机(status → orgs → repos → branches → files → file content → save → unlink);store.test.ts 则穷举了 store 的 hydrate、persist、CRUD 与异常分支。任何新成员只需读一遍测试,就能在不看实现的情况下掌握系统行为契约——这正是测试作为文档的最高价值形态。
延伸阅读:测试配置详见 vitest.config.ts 与 playwright.config.ts;全局测试环境与 mock 见 vitest.setup.ts;单元测试样本见 tests/lib/、tests/store/、tests/hooks/、tests/components/;路由集成测试见 tests/routes/;端到端测试见 tests/e2e/。
- 前端
- 开发工具
【免费下载链接】dillinger
The last Markdown editor, ever.
相关推荐
ag-kit 测试模式实践指南:从测试金字塔到 Mock 策略的完整落地方案
ag kit 测试模式实践指南:从测试金字塔到 Mock 策略的完整落地方案 本文以 ag kit 仓库中 testing patterns 技能文档 http
人工智能AI 技能GE图引擎SetDataType API文档
SetDataType<a name="ZH CN_TOPIC_0000002519215175" </a 产品支持情况<a name="section7891
人工智能深度学习模型编译模型优化编译器AscendDillinger 项目测试工程师指南:从测试金字塔到 Playwright E2E 的完整落地实践
Dillinger 项目测试工程师指南:从测试金字塔到 Playwright E2E 的完整落地实践 本篇技术指南以 Dillinger 开源仓库(GitHub
前端开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考