news 2026/9/24 16:51:19

Presto UI 前端测试体系深度指南:从 Jest 配置到测试工具链的完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Presto UI 前端测试体系深度指南:从 Jest 配置到测试工具链的完整实战
  • 大数据
  • 数据库
  • 后端

【免费下载链接】presto

The official home of the Presto distributed SQL query engine for big data

项目地址:https://gitcode.com/gh_mirrors/pre/presto
点击查看免费下载

Presto 分布式 SQL 查询引擎的 Web 管理界面(presto-ui)内置了一套完整的 React 组件测试基础设施,涵盖 Jest 配置、公共测试工具函数、Mock 体系与测试数据工厂。本文以presto-ui/src/__tests__/README.md为骨架,结合仓库中jest.config.jssetupTests.tstestUtils.tsxsetupHelpers.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:cijest --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,因此从该文件导入的renderscreen与直接使用 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 助手clickButtontypeIntoInput),其内部基于@testing-library/user-event模拟;
  • 仅需触发状态变更、不关心交互细节时,使用sync 助手clickButtonSyncsetInputValue),内部基于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()(覆盖matchMediahljsclipboarddocument等浏览器 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/目录下,而不是模块根目录)。核心配置项包括:

  • presetts-jest,配合transform规则(TS/TSX 走ts-jest,JS/JSX 走babel-jest)同时支持 TS 与 JS 测试文件;
  • testEnvironmentjsdom,模拟浏览器 DOM;
  • testMatch:匹配**/__tests__/**/*.+(ts|tsx|js|jsx)以及*.test.*/*.spec.*文件;
  • setupFilesAfterEnv<rootDir>/setupTests.ts,即全局测试初始化;
  • moduleNameMapper:将 CSS/LESS/SCSS/SASS 映射为identity-obj-proxy,避免样式导入导致测试失败;
  • transformIgnorePatterns:白名单放行dagre-d3-esd3等 ESM 依赖,确保它们可被转换;
  • collectCoverageFrom / testPathIgnorePatterns:覆盖统计与测试发现均排除sql-parserstatictemplates以及__tests__下的工具、Mock、Fixtures 目录(这些是基础设施而非测试用例);
  • 覆盖率阈值当前被注释禁用(README 注释为"先保证测试通过率,后续再开启"),覆盖率报告格式为textlcovhtml

依赖版本

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.1ts-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

经验法则:除非在专门验证用户交互行为,否则优先使用同步助手(clickButtonSyncsetInputValue)。

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

项目地址:https://gitcode.com/gh_mirrors/pre/presto
点击查看免费下载

相关推荐

上一篇:Audacity:免费多轨音频编辑器,十分钟修干净一条播客
下一篇:深蓝词库转换:终极输入法词库兼容解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 16:47:26

Netty 4.1 使用 Protobuf 传输数据:itstack-demo-netty 中级拓展篇二实战解析

文档教程后端 【免费下载链接】CodeGuide :books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总&#xff0c;旨在为大家提供一个清晰详细的学习教程&#xff0c;侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助&#xff0c;请给予支持(关注、…

作者头像 李华