- 大数据
- 数据库
- 后端
【免费下载链接】presto
The official home of the Presto distributed SQL query engine for big data
Presto 分布式 SQL 查询引擎的 Web 管理界面(presto-ui)内置了一套完整的 React 组件测试基础设施,涵盖 Jest 配置、公共测试工具函数、Mock 体系与测试数据工厂。本文以presto-ui/src/__tests__/README.md为骨架,结合仓库中jest.config.js、setupTests.ts、testUtils.tsx、setupHelpers.ts等真实源码,系统讲解该测试体系的设计与用法,帮助读者快速上手编写稳定、高效、可维护的 Presto UI 测试用例。
快速开始:运行与编写你的第一个测试
运行测试的命令
在presto-ui目录下(package.json中的 scripts 已定义好全部测试命令)执行:
# 运行全部测试 yarn test # 监听模式运行(文件变更自动重跑) yarn test:watch # 带覆盖率统计运行 yarn test:coverage # 只运行指定测试文件 yarn test QueryList.test.jsx此外,presto-ui/src/package.json还额外提供了面向 CI 的命令test:ci(jest --ci --coverage --maxWorkers=2),适用于流水线场景。注意package.json要求 Node.js 版本>=22.15.1。
编写第一个测试
测试文件一般与被测组件同级放置(如components/MyComponent.test.tsx),并统一从src/__tests__下导入测试基础设施:
import { render, screen } from "../__tests__/utils/testUtils"; import { setupCommonMocks } from "../__tests__/utils/setupHelpers"; import { createMockQuery } from "../__tests__/fixtures"; import MyComponent from "./MyComponent"; describe("MyComponent", () => { setupCommonMocks(); it("renders correctly", () => { const query = createMockQuery(); render(<MyComponent query={query} />); expect(screen.getByText(/expected text/i)).toBeInTheDocument(); }); });testUtils.tsx内部会export * from "@testing-library/react"并额外导出userEvent,因此从该文件导入的render、screen与直接使用 Testing Library 完全等价。
测试目录结构
按presto-ui/src/__tests__/README.md的约定,测试相关代码按职责划分为三层:
src/ ├── __tests__/ # 测试基础设施(不含测试用例本身) │ ├── fixtures/ # 测试数据工厂 │ │ ├── index.ts # Barrel 统一出口 │ │ ├── queryFixtures.js │ │ ├── infoFixtures.ts │ │ ├── stageFixtures.ts │ │ └── clusterFixtures.ts │ ├── mocks/ # Mock 工具 │ │ ├── index.ts # Barrel 统一出口 │ │ ├── apiMocks.ts # Fetch API 拦截 │ │ ├── jqueryMock.ts # jQuery 选择性 Mock │ │ └── browserMocks.ts # 浏览器 API Mock │ ├── utils/ # 测试工具函数 │ │ ├── testUtils.tsx # 通用断言/交互助手 │ │ └── setupHelpers.ts # 通用 setup 模式 │ └── README.md # 本文档 ├── components/ │ └── MyComponent.test.tsx └── router/ └── QueryList.test.jsx该目录结构在仓库中已完整落地,fixtures/、mocks/、utils/三个子目录及对应文件均可直接在presto-ui/src/__tests__/下找到。三个目录均提供index.ts作为 barrel 出口,测试代码中统一使用../__tests__/mocks、../__tests__/fixtures、../__tests__/utils/testUtils形式的导入,无需关心内部文件拆分。
测试工具函数(testUtils)
presto-ui/src/__tests__/utils/testUtils.tsx提供了一组覆盖加载状态、文本匹配、下拉框、表单、定时器与交互等待的公共助手。以下为 README 中的完整用法:
加载状态助手
import { waitForLoadingToFinish, expectLoading } from "../__tests__/utils/testUtils"; // 等待 loading 指示消失(内部轮询直到页面上不再出现 /loading/i 文本) render(<Component />); await waitForLoadingToFinish(); // 断言 loading 正在展示 render(<Component />); expectLoading();源码层面,waitForLoadingToFinish实现为waitFor(() => expect(screen.queryByText(/loading/i)).not.toBeInTheDocument()),expectLoading则断言/loading/i文本存在(见 testUtils.tsx)。
文本内容精确匹配器
当目标文本被拆散到多个子元素中时,普通getByText无法精确匹配整段文本,此时使用基于textContent的精确匹配:
import { findByTextContent, getByTextContent, queryByTextContent } from "../__tests__/utils/testUtils"; // 按完整文本内容异步查找 const element = await findByTextContent("Total: 100"); // 按完整文本内容同步获取(找不到直接抛错) const element = getByTextContent("Total: 100"); // 查询(找不到返回 null,便于反向断言) const element = queryByTextContent("Total: 100");三者的底层实现均通过screen.getByText((content, element) => element?.textContent === text)这类回调式匹配器完成(见 testUtils.tsx)。
下拉框助手
import { clickDropdownOption, selectDropdownByRole } from "../__tests__/utils/testUtils"; // 点击文本为 "Sort By" 的下拉框,再选择 "Name" 选项 await clickDropdownOption("Sort By", "Name"); // 按 ARIA role 定位下拉框(如 combobox)并选择选项 await selectDropdownByRole("combobox", "Option 1");表单交互助手
import { typeIntoInput, setInputValue, clickButton, clickButtonSync, appendToInput, } from "../__tests__/utils/testUtils"; // 真实输入(先 clear 再逐键输入,模拟用户行为) await typeIntoInput(/search/i, "test query"); // 直接赋值(走 fireEvent.change,速度快,适合非交互测试) setInputValue(/search/i, "test query"); // 真实点击按钮(按可访问名称定位) await clickButton(/submit/i); // 同步点击(fireEvent.click,更快) clickButtonSync(/submit/i); // 追加输入(不清空已有内容) await appendToInput(/search/i, " more text");何时用 async 版本、何时用 sync 版本:
- 测试真实用户交互行为(如输入、点击的完整事件序列)时,使用async 助手(
clickButton、typeIntoInput),其内部基于@testing-library/user-event模拟; - 仅需触发状态变更、不关心交互细节时,使用sync 助手(
clickButtonSync、setInputValue),内部基于fireEvent,速度更快。
定时器助手
import { advanceTimersAndWait, typeWithDebounce } from "../__tests__/utils/testUtils"; // 推进定时器并等待 React 更新(常用于防抖场景) await typeIntoInput(/search/i, "test"); await advanceTimersAndWait(300); // 等待 300ms 防抖触发 // 输入并自动处理防抖 const input = screen.getByPlaceholderText(/search/i); await typeWithDebounce(input, "test", 300);需要特别说明的是:advanceTimersAndWait依赖 Jest 假定时器。若未启用,它会抛出一条带引导信息的错误,提示先调用setupFakeTimers()或setupIntegrationTest()(见 testUtils.tsx)。这正是 README 在「Configuration」章节中专门压制假定时器告警的原因。
交互等待助手
import { clickAndWait, clickAndWaitForRemoval } from "../__tests__/utils/testUtils"; // 点击后等待某段文本出现(如成功提示) await clickAndWait(button, /success/i); // 点击后等待元素从 DOM 中消失(如删除按钮) await clickAndWaitForRemoval(deleteButton);组件级 Setup 助手(setupHelpers)
presto-ui/src/__tests__/utils/setupHelpers.ts将常见测试的前置准备(Mock 清理、浏览器 API、假定时器)封装为可在describe块中直接调用的模式。源码中提供了三个通用组合(见 setupHelpers.ts):
import { setupCommonMocks, setupFakeTimers, setupIntegrationTest } from "../__tests__/utils/setupHelpers"; // 通用 Mock 组合:beforeEach 中 clearAllMocks + setupAllBrowserMocks describe("MyComponent", () => { setupCommonMocks(); it("test", () => { /* ... */ }); }); // 假定时器组合:beforeEach 启用、afterEach 恢复真实定时器 describe("MyComponent", () => { setupFakeTimers(); it("test with timers", () => { /* ... */ }); }); // 集成测试组合:同时启用假定时器并清理 Mock describe("MyComponent Integration", () => { setupIntegrationTest(); it("integration test", async () => { /* ... */ }); });README 还记录了针对 Presto UI 核心页面的专用 setup 模式(其组件级实现位于setupHelpers.ts的 Custom Setup Builders 区域):
QueryList Setup
import { setupQueryListTest } from "../__tests__/utils/setupHelpers"; import { createRunningQuery, createFinishedQuery } from "../__tests__/fixtures"; const { mockQueries } = setupQueryListTest([ createRunningQuery({ queryId: "q1" }), createFinishedQuery({ queryId: "q2" }), ]); render(<QueryList />);QueryDetail Setup
import { setupQueryDetailTest } from "../__tests__/utils/setupHelpers"; import { createMockQuery } from "../__tests__/fixtures"; const { mockQuery } = setupQueryDetailTest(createMockQuery({ queryId: "test_123" }), { includeStages: true }); render(<QueryDetail queryId="test_123" />);ClusterHUD Setup
import { setupClusterHUDTest } from "../__tests__/utils/setupHelpers"; const { clusterData } = setupClusterHUDTest({ clusterData: { runningQueries: 10 }, workerData: [{ workerId: "w1", state: "ACTIVE" }], }); render(<ClusterHUD />);Mock 体系
Mock 统一从__tests__/mocks导入,覆盖 jQuery、Fetch API 与浏览器 API 三类外部依赖。
jQuery Mock
Presto UI 使用 jQuery 4.0.0(见package.json依赖声明),测试采用"保留真实 jQuery、选择性 Mock 插件"的策略:
import { mockJQueryGet, mockJQueryAjax } from "../__tests__/mocks"; // Mock $.get mockJQueryGet("/api/query", { queryId: "123", state: "RUNNING" }); // Mock $.ajax mockJQueryAjax({ url: "/api/query", success: (data) => data, error: (xhr, status, error) => error, });Fetch API Mock
import { mockFetchByUrl } from "../__tests__/mocks"; import { createMockQuery } from "../__tests__/fixtures"; mockFetchByUrl({ "/v1/query": [createMockQuery()], "/v1/info": { runningQueries: 5 }, });浏览器 API Mock
import { setupAllBrowserMocks } from "../__tests__/mocks"; beforeEach(() => { setupAllBrowserMocks(); });浏览器 API Mock 的全局装配发生在 setupTests.ts:该文件在每个测试文件执行前,依次调用setupAllBrowserMocks()(覆盖matchMedia、hljs、clipboard、document等浏览器 API)、setupFetchMock()(集中的 Fetch 拦截)与setupJQuery()(选择性 jQuery Mock)。这意味着大多数测试无需手动配置这些 Mock,直接通过组件级 setup 即可使用。
测试数据工厂(Fixtures)
Fixtures 为测试数据提供"带合理默认值"的工厂函数,统一从__tests__/fixtures导入。
查询类 Fixtures
import { createMockQuery, createRunningQuery, createFinishedQuery, createFailedQuery } from "../__tests__/fixtures"; // 基础查询对象 const query = createMockQuery(); // 运行中查询(可覆盖字段) const runningQuery = createRunningQuery({ queryId: "custom_123", query: "SELECT * FROM users", }); // 已完成查询 const finishedQuery = createFinishedQuery({ elapsedTime: "5.2s", }); // 失败查询 const failedQuery = createFailedQuery({ errorType: "USER_ERROR", errorCode: { name: "SYNTAX_ERROR" }, });其他 Fixtures
import { createMockInfo, createMockStage, createMockCluster } from "../__tests__/fixtures"; const info = createMockInfo({ runningQueries: 10 }); const stage = createMockStage({ stageId: "0" }); const cluster = createMockCluster({ activeWorkers: 5 });这些工厂函数与createMockQuery一样,都接受"覆盖对象"参数,未提供的字段自动取默认值,从而避免在测试中手写冗长的完整数据对象。
测试模式(Testing Patterns)
单元测试
import { render, screen } from "../__tests__/utils/testUtils"; import { setupCommonMocks } from "../__tests__/utils/setupHelpers"; import { createMockQuery } from "../__tests__/fixtures"; import QueryListItem from "./QueryListItem"; describe("QueryListItem", () => { setupCommonMocks(); it("displays query ID", () => { const query = createMockQuery({ queryId: "test_123" }); render(<QueryListItem query={query} />); expect(screen.getByText("test_123")).toBeInTheDocument(); }); });集成测试
import { render, screen } from "../__tests__/utils/testUtils"; import { setupIntegrationTest, setupQueryListTest } from "../__tests__/utils/setupHelpers"; import { clickButton } from "../__tests__/utils/testUtils"; import { createRunningQuery, createFinishedQuery } from "../__tests__/fixtures"; import QueryList from "./QueryList"; describe("QueryList Integration", () => { setupIntegrationTest(); it("filters queries by state", async () => { setupQueryListTest([createRunningQuery({ queryId: "q1" }), createFinishedQuery({ queryId: "q2" })]); render(<QueryList />); await clickButton(/FINISHED/i); expect(screen.getByText("q2")).toBeInTheDocument(); expect(screen.queryByText("q1")).not.toBeInTheDocument(); }); });定时器测试(防抖场景)
import { render, screen } from "../__tests__/utils/testUtils"; import { setupFakeTimers } from "../__tests__/utils/setupHelpers"; import { typeIntoInput, advanceTimersAndWait } from "../__tests__/utils/testUtils"; describe("SearchComponent", () => { setupFakeTimers(); it("debounces search input", async () => { render(<SearchComponent />); await typeIntoInput(/search/i, "test"); await advanceTimersAndWait(300); expect(screen.getByText(/results for "test"/i)).toBeInTheDocument(); }); });测试环境配置
Jest 配置要点
测试配置位于 jest.config.js(注意:配置文件在src/目录下,而不是模块根目录)。核心配置项包括:
- preset:
ts-jest,配合transform规则(TS/TSX 走ts-jest,JS/JSX 走babel-jest)同时支持 TS 与 JS 测试文件; - testEnvironment:
jsdom,模拟浏览器 DOM; - testMatch:匹配
**/__tests__/**/*.+(ts|tsx|js|jsx)以及*.test.*/*.spec.*文件; - setupFilesAfterEnv:
<rootDir>/setupTests.ts,即全局测试初始化; - moduleNameMapper:将 CSS/LESS/SCSS/SASS 映射为
identity-obj-proxy,避免样式导入导致测试失败; - transformIgnorePatterns:白名单放行
dagre-d3-es、d3等 ESM 依赖,确保它们可被转换; - collectCoverageFrom / testPathIgnorePatterns:覆盖统计与测试发现均排除
sql-parser、static、templates以及__tests__下的工具、Mock、Fixtures 目录(这些是基础设施而非测试用例); - 覆盖率阈值当前被注释禁用(README 注释为"先保证测试通过率,后续再开启"),覆盖率报告格式为
text、lcov、html。
依赖版本
README 记载的测试技术栈为Jest 29.7.0(ts-jest + babel-jest)、React Testing Library 16.1.0、@testing-library/user-event 14.5.2;而presto-ui/src/package.json中实际声明的版本为jest ^30.3.0、@testing-library/react ^16.3.2、@testing-library/user-event ^14.6.1、ts-jest ^29.4.9,并配套@testing-library/jest-dom ^6.9.1提供toBeInTheDocument等自定义匹配器。实际运行时以package.json锁定的依赖为准。
假定时器告警压制
在启用假定时器的测试中,异步助手调用jest.advanceTimersByTime()会触发 Jest 的已知误报警告("A function to advance timers was called but the timers APIs are not replaced…")。setupTests.ts 在 Jest 环境(通过process.env.JEST_WORKER_ID判断)下对该告警进行精确前缀匹配并静默,同时保留其他所有合法告警:
const originalWarn = console.warn; const JEST_FAKE_TIMER_WARNING = "A function to advance timers was called but the timers APIs are not replaced with fake timers. Call `jest.useFakeTimers()` in this test file or enable fake timers for all tests by setting 'fakeTimers': {'enableGlobally': true} in Jest configuration file."; if (process.env.JEST_WORKER_ID !== undefined) { console.warn = (...args: any[]) => { const message = args[0]?.toString() || ""; if (message.startsWith(JEST_FAKE_TIMER_WARNING)) { return; } originalWarn.apply(console, args); }; }该设计刻意使用"完整前缀 + 仅限 Jest 环境"的双重约束,避免误伤测试中对console.warn的 spy 断言。
最佳实践
1. 优先使用 Setup 助手
✅推荐:使用 setup 助手统一前置准备
describe("QueryList", () => { setupCommonMocks(); it("test", () => { setupQueryListTest([createMockQuery()]); render(<QueryList />); }); });❌不推荐:每个用例手动重复拼装
it("test", () => { jest.clearAllMocks(); setupAllBrowserMocks(); mockFetchByUrl({ "/v1/query": [createMockQuery()] }); render(<QueryList />); });2. 优先使用测试工具函数
✅推荐:语义化的一行调用
await clickButton(/submit/i); await typeIntoInput(/search/i, "test");❌不推荐:裸写 Testing Library 底层交互
const button = screen.getByRole("button", { name: /submit/i }); await userEvent.click(button); const input = screen.getByPlaceholderText(/search/i); await userEvent.type(input, "test");3. 优先使用 Fixtures
✅推荐:工厂函数 + 少量覆盖
const query = createRunningQuery({ queryId: "test" });❌不推荐:手写含大量冗余字段的对象
const query = { queryId: "test", state: "RUNNING", // ... 50 more properties };4. 测试用户可见行为
✅推荐:断言用户看到的内容
expect(screen.getByText("RUNNING")).toBeInTheDocument();❌不推荐:断言组件内部实现细节
expect(component.state.queries[0].state).toBe("RUNNING");5. 按需选择 sync / async 助手
✅推荐:不关心交互真实性的场景用同步助手提速
clickButtonSync(/submit/i); setInputValue(/search/i, "test"); await advanceTimersAndWait(300);❌不推荐:简单状态变更也滥用慢速异步助手
await clickButton(/submit/i); // Slower, not needed for simple state changes await typeIntoInput(/search/i, "test"); // Much slower经验法则:除非在专门验证用户交互行为,否则优先使用同步助手(clickButtonSync、setInputValue)。
6. 使用描述性测试命名
✅推荐:清楚表达意图
it("displays error message when API call fails", () => { // test });❌不推荐:含义模糊
it("works", () => { // test });实战对比:使用助手前 vs 使用助手后
以"按状态过滤查询列表"这一 Presto UI 核心交互为例,对比重构前后的测试代码。
改造前(未使用助手)
it("filters queries by state", async () => { const queries = [createRunningQuery({ queryId: "q1" }), createFinishedQuery({ queryId: "q2" })]; jest.clearAllMocks(); setupAllBrowserMocks(); mockFetchByUrl({ "/v1/query": queries }); render(<QueryList />); const finishedButton = screen.getByRole("button", { name: /FINISHED/i }); await userEvent.click(finishedButton); expect(screen.getByText("q2")).toBeInTheDocument(); expect(screen.queryByText("q1")).not.toBeInTheDocument(); });改造后(使用助手)
describe("QueryList", () => { setupCommonMocks(); it("filters queries by state", async () => { setupQueryListTest([createRunningQuery({ queryId: "q1" }), createFinishedQuery({ queryId: "q2" })]); render(<QueryList />); await clickButton(/FINISHED/i); expect(screen.getByText("q2")).toBeInTheDocument(); expect(screen.queryByText("q1")).not.toBeInTheDocument(); }); });改进效果:代码量减少约 30%,意图更清晰,模式可复用,维护成本显著降低。
总结与深入阅读路径
Presto UI 的测试体系遵循"基础设施与用例分离"的组织方式:__tests__/utils提供通用断言与交互助手,__tests__/mocks封装外部依赖模拟,__tests__/fixtures提供带默认值的数据工厂,setupTests.ts完成全局环境装配,jest.config.js统一定义测试发现与转换规则。理解这套体系后,为 Presto UI 新增组件测试只需三步:setupCommonMocks()声明前置、createMockXxx()构造数据、render+ 助手函数完成交互与断言。
感兴趣的读者可以继续深入以下仓库文件:
- tests/utils/testUtils.tsx — 全部公共测试助手实现
- tests/utils/setupHelpers.ts — 通用与组件级 setup 模式
- tests/mocks/apiMocks.ts — Fetch API 拦截实现
- tests/mocks/jqueryMock.ts — jQuery 选择性 Mock 实现
- tests/mocks/browserMocks.ts — 浏览器 API Mock 实现
- tests/fixtures/queryFixtures.js — 查询数据工厂实现
- jest.config.js — Jest 完整配置
- setupTests.ts — 全局测试初始化与告警压制
- package.json — 测试相关依赖与脚本声明
- 大数据
- 数据库
- 后端
【免费下载链接】presto
The official home of the Presto distributed SQL query engine for big data
相关推荐
Paint-board形状绘制详解:从基础图形到复杂箭头的高级技巧
Paint board形状绘制详解:从基础图形到复杂箭头的高级技巧 Paint board是一款功能强大的多端绘图板应用,集成了丰富的创意画笔,为用户带来全新的
前端Material UI 测试体系全指南:从单元测试到端到端测试的全链路工程实践
Material UI 测试体系全指南:从单元测试到端到端测试的全链路工程实践 本指南基于 Material UI 官方仓库的 test/README.md h
前端UI组件设计系统Snowpack与Jest集成:前端测试完整配置指南
Snowpack与Jest集成:前端测试完整配置指南 你是否还在为前端项目配置测试环境而烦恼?本文将带你一步到位解决Snowpack与Jest的集成难题,从基础
前端开发工具前端构建
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考