恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Langflow 前端单元测试 Mock 实践:基于 Jest 的完整 Mock 指南(API、Zustand、React Router 与 React Query)
首页
资讯中心
/
Langflow 前端单元测试 Mock 实践:基于 Jest 的完整 Mock 指南(API、Zustand、React Router 与 React Query)
Langflow 前端单元测试 Mock 实践:基于 Jest 的完整 Mock 指南(API、Zustand、React Router 与 React Query)
发布时间:2026/9/7 19:05:12
Langflow 前端单元测试 Mock 实践基于 Jest 的完整 Mock 指南API、Zustand、React Router 与 React Query【免费下载链接】langflowLangflow is a powerful tool for building and deploying AI-powered agents and workflows.项目地址: https://gitcode.com/GitHub_Trending/la/langflow本文基于 Langflow 仓库内置的前端测试 Mock 指南 .agents/skills/frontend-testing/references/mocking.md 展开。Langflow 前端src/frontend使用 Jest jsdom ts-jest 作为单元测试框架且明确规定所有 Mock 一律使用 Jest API禁止使用 Vitestvi.*API。读完本文你将掌握 Langflow 前端测试中 Mock Axios API 实例、Zustand Store、React Router 钩子、React Query、Context Provider 及第三方可视化组件如xyflow/react的标准写法并理解jest.setup.js/setupTests.ts中已内置的全局 Mock避免重复 Mock 导致的测试污染。测试环境总览为什么必须用 Jest 而不是 VitestLangflow 前端的测试配置集中在 src/frontend/jest.config.js几个关键决策点决定了后续所有 Mock 写法的边界preset: ts-jest、testEnvironment: jsdomTypeScript/TSX 测试在 jsdom 环境中运行moduleNameMapper中^/(.*)$: rootDir/src/$1所有/导入路径都会映射到src/frontend/src/Mock 模块路径时也应使用同一别名setupFiles: [rootDir/jest.setup.js]与setupFilesAfterEach: [rootDir/src/setupTests.ts]分两级注入全局 Mock详见下文转换器是自定义的 src/frontend/transform-import-meta.js它在 ts-jest 之前把源码中的import.meta.env文本替换为process.env以适配 Vite 风格代码在 JestCommonJS下的运行。运行入口在 src/frontend/package.json 的 scripts 中test即jest、test:coverage、test:watch。由于框架是 Jest文档给出一份 Vitest → Jest 的 API 对照表左列禁用右列使用VitestDO NOT USEJestUSE THISvi.fn()jest.fn()vi.mock()jest.mock()vi.spyOn()jest.spyOn()vi.mocked()jest.mocked()vi.clearAllMocks()jest.clearAllMocks()vi.useFakeTimers()jest.useFakeTimers()vi.useRealTimers()jest.useRealTimers()vi.advanceTimersByTime()jest.advanceTimersByTime()需要说明src/frontend/jest.config.js 中有一段针对 unified/Markdown 纯 ESM 包micromark、react-markdown、vfile等的白名单转换配置且注释明确指出该转换器必须与 ts-jest 主转换共用同一实例否则会破坏 ts-jest 对.ts/.tsx测试中jest.mock工厂函数里 mock 变量引用的**自动提升hoisting**行为——这是理解后文“Mock 工厂里可以安全引用外部变量”这一惯例的基础。已内置的全局 Mock先查再 MockMock 指南的第一原则先检查jest.setup.js与setupTests.ts已全局 Mock 的模块不要重复 Mock除非需要不同行为。这些全局 Mock 分为两个文件一、jest.setup.jssetupFiles模块级 stubsrc/frontend/jest.setup.js 中与 Mock 指南直接对应的条目及源码依据已全局 Mock 的模块行为源码位置radix-ui/react-form所有导出Field/Label/Control/Message/Submit/Root直接渲染 childrenjest.setup.js#L109-L117react-markdown渲染nulljest.setup.js#L119remark-gfm、remark-math、rehype-mathjax/browserno-op 插件 stub纯 ESM在 Jest 下无法解析jest.setup.js#L120-L127lucide-react/dynamicIconImports空对象且使用{ virtual: true }虚拟模块jest.setup.js#L140/components/common/genericIconComponent渲染nulljest.setup.js#L143-L146/icons/BotMessageSquareBotMessageSquareIcon渲染null该图标为 JSX 文件Jest 下转换有兼容问题jest.setup.js#L149-L152/stores/darkStore返回固定默认状态dark: false、stars: 0等jest.setup.js#L164-L181localStorage/sessionStorage全部方法为jest.fn()的空 stubjest.setup.js#L84-L94此外该文件还全局 Mock 了react-i18nextt()直接读取src/locales/en.json返回英文原文避免真实 i18n 初始化、/components/common/shadTooltipComponent只渲染 children跳过 TooltipProvider context 依赖、/controllers/API/queries/flows/use-get-note-translations返回空数据避免 QueryClientProvider 依赖并补齐了crypto.webcrypto、URL、TextEncoder/TextDecoderjsdom 不暴露而 react-router 在模块加载时会读取等环境补丁见 jest.setup.js#L65-L82。如果某个测试确实需要darkStore的真实实现可用jest.unmock(/stores/darkStore);二、setupTests.tssetupFilesAfterEachDOM/浏览器 API stubsrc/frontend/src/setupTests.ts 全局 Mock 了以下浏览器 API同样不要重复 MockResizeObserversetupTests.ts#L7-L12IntersectionObserversetupTests.ts#L14-L19window.matchMediasetupTests.ts#L21-L34该文件还通过jest-axe扩展了expect(...).toHaveNoViolations()断言用于可访问性测试并在beforeAll/afterAll中抑制了特定的 React 弃用警告ReactDOM.render is deprecated、componentWillReceiveProps has been renamed避免噪音污染测试输出setupTests.ts#L36-L65。API MockingMock Axios 实例与 Query Hooks了解被测对象Langflow 的 API 模块结构Langflow 前端所有 HTTP 请求经过一个集中配置的 Axios 实例。在 src/frontend/src/controllers/API/api.tsx 中// Create a new Axios instance const api: AxiosInstance axios.create({ baseURL: baseURL, withCredentials: getAxiosWithCredentials(), });该文件通过命名导出暴露api、ApiInterceptor和performStreamingRequest见 api.tsx#L431。真实实例上还挂了请求/响应拦截器请求侧注入自定义 headers 与x-langflow-client标识、去重与中止控制响应侧处理 401/403 时的 refresh-token 重试与登出流程api.tsx#L56-L306。在单元测试中 Mock 掉这个模块正是为了让测试不触发真实网络与认证副作用。Mocking Axios 调用Mock 指南给出的标准写法是 Mock/controllers/API/api模块本身import api from /controllers/API/api; jest.mock(/controllers/API/api, () ({ __esModule: true, default: { get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn(), delete: jest.fn(), }, })); describe(MyComponent, () { beforeEach(() { jest.clearAllMocks(); }); it(should fetch data on mount, async () { jest.mocked(api.get).mockResolvedValueOnce({ data: { items: [{ id: 1, name: Test }] }, }); render(MyComponent /); await waitFor(() { expect(screen.getByText(Test)).toBeInTheDocument(); }); expect(api.get).toHaveBeenCalledWith(/api/v1/items); }); it(should handle API errors, async () { jest.mocked(api.get).mockRejectedValueOnce(new Error(Network error)); render(MyComponent /); await waitFor(() { expect(screen.getByText(/error/i)).toBeInTheDocument(); }); }); });要点拆解工厂函数中__esModule: true必不可少否则默认导出解析会出错见“常见陷阱”第 2 条jest.mocked(api.get)是 Jest 提供的类型安全包装让mockResolvedValueOnce等调用获得正确的类型推导每个测试前jest.clearAllMocks()防止上一个测试的mockResolvedValue残留造成测试间污染。一个值得注意的实现细节从源码看api模块当前以命名导出方式暴露export { ApiInterceptor, api, performStreamingRequest }。如果你的测试目标文件实际是import { api } from /controllers/API/apiMock 工厂应相应用named export形式{ api: {...}, ApiInterceptor: jest.fn(), performStreamingRequest: jest.fn() }。Mock 指南中的default写法适合同时 Mock 默认导出与具名导出的模块无论哪种核心思路一致——只替换被测组件真正 import 的那几个成员其余用jest.requireActual透传。Mocking 具体的 React Query 钩子对于使用 src/frontend/src/controllers/API/queries/ 下 React Query 钩子的组件直接 Mock 钩子模块即可。以 flows 域为例仓库中真实的钩子文件包括 use-get-flow.ts、use-post-add-flow.ts、use-patch-update-flow.ts 等且同目录下已有tests/ 中的配套测试如use-get-refresh-flows-query.test.ts、use-patch-update-flow.test.ts可作为参照。Mock 写法jest.mock(/controllers/API/queries/flows, () ({ useGetFlowsQuery: jest.fn().mockReturnValue({ data: [{ id: flow-1, name: My Flow }], isLoading: false, error: null, refetch: jest.fn(), }), }));Mocking 单个 API 函数/controllers/API的 index.ts 汇聚了大量具名异步函数如createApiKey、saveFlowStore等。当组件依赖这类具名导出函数时jest.mock(/controllers/API, () ({ getFlows: jest.fn().mockResolvedValue([]), saveFlow: jest.fn().mockResolvedValue({ id: new-flow }), deleteFlow: jest.fn().mockResolvedValue(undefined), }));Zustand Store Mocking三种方案Langflow 前端使用 Zustandpackage.json 中为^4.5.2管理状态没有全局 Zustand 自动 Mock。文档给出三个选项按优先级排列方案 1推荐使用真实 Store setState()绝大多数场景应使用真实 Store 并显式重置状态。以 src/frontend/src/stores/alertStore.ts 为例其真实状态形状见 alertStore.ts#L7-L13为errorData: { title: , list: [] }、noticeData: { title: , link: }、successData: { title: }、notificationCenter: false、notificationList: []、tempNotificationList: []——与文档示例完全一致import useAlertStore from /stores/alertStore; describe(Alert-dependent component, () { beforeEach(() { // Reset store to known state before each test useAlertStore.setState({ errorData: { title: , list: [] }, noticeData: { title: , link: }, successData: { title: }, notificationCenter: false, notificationList: [], tempNotificationList: [], }); }); it(should display error notification, () { // Pre-set store state useAlertStore.setState({ errorData: { title: Something went wrong, list: [Detail] }, }); render(NotificationBanner /); expect(screen.getByText(Something went wrong)).toBeInTheDocument(); }); });该方案的优势在于setErrorData等 action 内部还会联动addNotificationToHistory/addNotificationToTempListalertStore.ts#L47-L57Mock Store 会丢失这类真实联动逻辑。方案 2Mock 整个 Store 模块当真实 Store 初始化复杂、或依赖import.metaJest 下由 transform 改写但仍可能有坑时可以整体替换模块。关键技巧是让默认导出变成一个支持 selector 的函数兼容useStore(selector)的两种调用方式const mockFlowStore { nodes: [], edges: [], setNodes: jest.fn(), setEdges: jest.fn(), onNodesChange: jest.fn(), onEdgesChange: jest.fn(), onConnect: jest.fn(), }; jest.mock(/stores/flowStore, () ({ __esModule: true, default: (selector?: (state: any) any) selector ? selector(mockFlowStore) : mockFlowStore, })); describe(FlowCanvas, () { beforeEach(() { jest.clearAllMocks(); mockFlowStore.nodes []; mockFlowStore.edges []; }); it(should render nodes from the store, () { mockFlowStore.nodes [ { id: node-1, type: genericNode, data: { node: { display_name: OpenAI } }, position: { x: 0, y: 0 } }, ]; render(FlowCanvas /); expect(screen.getByText(OpenAI)).toBeInTheDocument(); }); });这里对应仓库中真实存在的 src/frontend/src/stores/flowStore.ts画布节点/边状态API 层也有useFlowStore.getState()的调用见 api.tsx#L287-L292。注意mockFlowStore定义在jest.mock工厂之外却能被工厂引用依赖的是 ts-jest 对mock前缀变量的提升处理——这也是 jest.config.js 中转换器配置注释特别强调“必须复用同一 ts-jest 实例”的原因。方案 3用renderHook直接测试 Store 本身当被测对象就是 Store而非依赖 Store 的组件时import { act, renderHook } from testing-library/react; import useMyStore from ../myStore; describe(useMyStore, () { beforeEach(() { useMyStore.setState({ count: 0 }); }); it(should increment count, () { const { result } renderHook(() useMyStore()); act(() { result.current.increment(); }); expect(result.current.count).toBe(1); }); });调用 action 必须包在act()中否则 React 会抛出“update not wrapped in act”的告警。React Router MockingLangflow 使用react-router-dom非 Next.js 路由。package.json 中实际版本为^7.18.2Mock 指南原文写作 v6v6/v7 在MemoryRouter、useNavigate、useParams、useSearchParams这些钩子上 API 一致下述写法均适用。注意 jest.setup.js#L76-L82 已为 jsdom 补齐TextEncoder/TextDecoder因为 react-router 在模块加载时会读取它们——缺失时所有导入react-router-dom的测试套件都会失败这正是该全局补丁存在的理由。用 MemoryRouter 包裹import { MemoryRouter } from react-router-dom; it(should render the page, () { render( MemoryRouter initialEntries{[/flows/flow-123]} FlowPage / /MemoryRouter, ); });Mock useNavigateconst mockNavigate jest.fn(); jest.mock(react-router-dom, () ({ ...jest.requireActual(react-router-dom), useNavigate: () mockNavigate, })); it(should navigate to flow page on click, async () { const user userEvent.setup(); render( MemoryRouter FlowCard flow{mockFlow} / /MemoryRouter, ); await user.click(screen.getByText(Open Flow)); expect(mockNavigate).toHaveBeenCalledWith(/flow/flow-123); });注意...jest.requireActual(react-router-dom)的展开只替换useNavigate其余导出MemoryRouter、Link等保留真实实现。Mock useParamsjest.mock(react-router-dom, () ({ ...jest.requireActual(react-router-dom), useParams: () ({ flowId: flow-123 }), }));Mock useSearchParamsjest.mock(react-router-dom, () ({ ...jest.requireActual(react-router-dom), useSearchParams: () [new URLSearchParams(tabsettings), jest.fn()], }));useSearchParams的返回值是二元组[searchParams, setSearchParams]两个都要给出。React Query MockingLangflow 前端使用tanstack/react-querypackage.json 中为^5.49.2v5。创建测试用 QueryClientimport { QueryClient, QueryClientProvider } from tanstack/react-query; function createTestQueryClient() { return new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0, }, mutations: { retry: false, }, }, }); } function renderWithQueryClient(ui: React.ReactElement) { const queryClient createTestQueryClient(); return render( QueryClientProvider client{queryClient}{ui}/QueryClientProvider, ); }retry: false和gcTime: 0是测试环境的关键前者保证失败查询立即报错而不是后台重试否则断言时序不可控后者让缓存即刻过期避免跨用例缓存命中。Mock useMutationjest.mock(tanstack/react-query, () ({ ...jest.requireActual(tanstack/react-query), useMutation: jest.fn().mockReturnValue({ mutate: jest.fn(), mutateAsync: jest.fn(), isPending: false, isError: false, error: null, data: undefined, }), }));字段名isPending是 v5 的命名v4 为isLoading与仓库锁定的 React Query v5 一致。Context Provider Mocking仓库中存在真实的 src/frontend/src/contexts/authContext.tsx。依赖 Context 的组件有两种 Mock 策略用 Provider 注入 Mock 值推荐作用域清晰import { AuthContext } from /contexts/authContext; const mockAuthContext { isAuthenticated: true, userData: { id: user-1, username: testuser }, login: jest.fn(), logout: jest.fn(), getAuthentication: jest.fn(), autoLogin: false, }; it(should show user name when authenticated, () { render( AuthContext.Provider value{mockAuthContext} UserMenu / /AuthContext.Provider, ); expect(screen.getByText(testuser)).toBeInTheDocument(); });直接 Mock useContext当无法在渲染树外层包裹 Provider例如被测组件被深层第三方库实例化时jest.mock(react, () ({ ...jest.requireActual(react), useContext: jest.fn().mockReturnValue({ isAuthenticated: true, userData: { username: testuser }, }), }));警告这会替换所有useContext调用包括 React 内部及其他 Context 的消费方因此只应在无法使用 Provider 包裹时使用且测试粒度必须很小。组件级 MockingMock 子组件当子组件复杂或有副作用如发起真实请求、渲染重型可视化替换为最小替身并保留关键交互契约props 回调jest.mock(/components/core/chatView/ChatView, () ({ __esModule: true, default: ({ onSend }: any) ( div>jest.mock(xyflow/react, () ({ ReactFlow: ({ children }: any) div>const originalLocation window.location; beforeEach(() { Object.defineProperty(window, location, { value: { ...originalLocation, href: http://localhost:3000, assign: jest.fn() }, writable: true, }); }); afterEach(() { Object.defineProperty(window, location, { value: originalLocation, writable: true, }); });Mock ClipboardObject.assign(navigator, { clipboard: { writeText: jest.fn().mockResolvedValue(undefined), readText: jest.fn().mockResolvedValue(clipboard content), }, });jsdom 默认不提供navigator.clipboard直接调用会抛错此写法以最小侵入方式补齐两个最常用的异步方法。常见陷阱Common PitfallsMock 指南最后列出 5 条高频错误逐条说明Mock 提升hoisting误解jest.mock()调用会被 Jest 自动提升到文件顶部无需手动放在 import 语句之前出于可读性惯例放前面是好的但不是必须。忘记__esModule: trueMock 带默认导出的 ES 模块时工厂返回值必须包含__esModule: true否则 Babel/ts-jest 的 interop 逻辑会把整个工厂对象当作命名空间包装导致default取不到。过度 MockOver-mocking只 Mock 必需的边界。如果真实模块在 jsdom 下能正常工作优先用真实实现——真实模块能发现集成问题Mock 只会固化你“以为”的行为。不重置 Mock每个测试前jest.clearAllMocks()防止mockResolvedValue等残留造成跨用例污染。重复 Mock 已全局 Mock 的模块动笔前先检查 jest.setup.js 与 setupTests.ts。重复定义同一模块的 Mock 可能因 setup 顺序差异产生难以排查的行为偏差。小结Langflow 前端 Mock 的决策路径把全文浓缩为一张决策表方便写测试时快速检索被测依赖首选方案退路方案Axios / 后端 APIMock/controllers/API/api模块Mock 具体 query 钩子或/controllers/API具名函数Zustand Store真实 Store setState()重置Mock 整个 store 模块selector 兼容函数测 store 本身用renderHook路由MemoryRouter包裹用jest.requireActual展开后替换单个钩子useNavigate/useParams/useSearchParamsReact Query自建retry: false, gcTime: 0的 QueryClientMockuseMutation等具体钩子ContextProvider 注入 mock value极端情况下 Mockreact的useContext重型子组件/三方库最小替身 保留 props 契约整体jest.mock模块如xyflow/react/components/ui/*、全局已 Mock 模块不 Mock真实渲染 / 先查 setup 文件—配合 src/frontend/jest.config.js 的testMatch约定src/**/__tests__/**/*.{test,spec}.{ts,tsx}与src/**/*.{test,spec}.{ts,tsx}把新测试文件放进被测模块旁的__tests__/目录即可被npm test直接拾取运行。【免费下载链接】langflowLangflow is a powerful tool for building and deploying AI-powered agents and workflows.项目地址: https://gitcode.com/GitHub_Trending/la/langflow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考