前端【免费下载链接】rematchThe Redux Framework项目地址https://gitcode.com/gh_mirrors/re/rematch点击查看免费下载Rematch 作为构建在 Redux 之上的轻量框架其 store 本质上就是一个标准的 Redux store因此测试几乎可以“开箱即用”无论是 Jest、Mocha、Ava 这类单元测试框架还是 Cypress、Testing Library 这类端到端测试方案都可以直接驱动 Rematch 应用。本篇基于仓库官方文档 Testing 展开结合 packages/core 的源码实现与官方测试套件完整讲清三类测试场景的可复制做法通过 store 测试 reducers 与 effects、脱离 store 直接测试 effect 函数、以及用 Testing Library 测试连接 Rematch store 的 React 组件。一、为什么 Rematch 不需要特殊测试基础设施Rematch 的 store 由init创建内部就是一个 Redux storerematch/core的 package.json 声明了redux: 4的 peer 依赖所有 dispatch 走的都是标准的modelName/actionName动作分发链路。这意味着断言可以直接调用store.getState()读取任意 model 的状态effect 通过中间件执行await掉store.dispatch.xxx.effectName()后状态即已同步完成可以直接断言组件测试与任何 React react-redux 项目的测试方式一致只需在渲染时提供Provider。官方文档在 Testing 中给出的建议是可以直接参考 packages/core/test 目录下的完整测试套件作为范例——这些用例基于 Jest 编写但思路适用于任何测试框架。仓库根目录的 jest.config.js 配置极简仅指定了一个setupFiles而 testsetup.js 中注册了一个全局监听// testsetup.js process.on(unhandledRejection, (err) { throw err })这个细节对 async effect 测试很重要effect 通常是异步函数如果 effect 内部的 Promise 被拒绝且未被捕获Jest 运行时会因这个全局监听而直接抛出错误并让测试失败——也就是说写测试时应该await掉所有 effect 调用既保证断言时机正确也避免悬空的未处理 rejection。仓库采用 Lerna workspaces 组织包与示例见根 package.json测试通过lerna run test在各包内执行核心包rematch/core使用dts testtsdx 封装的 Jest运行例如运行 effects 测试 等用例。二、测试前置准备Model、RootModel 与 Store官方文档的示例围绕一个最简的countmodel 展开以下三个文件是所有测试示例共享的“脚手架”完整继承自 Testingcount modelcount.tsimport { createModel } from rematch/core import { RootModel } from ./models export const count createModelRootModel()({ state: 0, reducers: { increment(state, payload: number) { return state payload }, }, effects: (dispatch) ({ incrementAsync(payload: number, state) { dispatch.count.increment(payload) }, }), })这里同时演示了 effect 的函数式写法effects接收dispatch作为参数从而在 effect 内部可以调用其他 model 的 action。RootModelmodels.tsimport { Models } from rematch/core import { count } from ./count export interface RootModel extends ModelsRootModel { count: typeof count } export const models: RootModel { count }Storestore.tsimport { init, RematchDispatch, RematchRootState } from rematch/core import { models, RootModel } from ./models export const store initRootModel({ models, }) export type Store typeof store export type Dispatch RematchDispatchRootModel export type RootState RematchRootStateRootModelRematchDispatchRootModel与RematchRootStateRootModel这两个泛型导出是组件层useDispatch/useSelector获得完整类型推断的关键测试组件时同样需要它们。三、Jest 测试一通过 Store 测试 Reducers 与 Effects这是最常用、也最贴近真实运行路径的测试方式为每个测试用例创建一个新的 storedispatch 后断言getState()。测试 reducer 路径经 effect 触发import { init } from rematch/core; import { models, RootModel } from ./models; describe([count] model, () { it(incrementAsync effect should increment given a payload, async () { const store initRootModel({ models, }); await store.dispatch.count.incrementAsync(3); const myModelData store.getState().count; expect(myModelData).toEqual(3); }); });要点每个用例init一个新 store避免用例之间共享状态造成断言污染incrementAsync是 effect其返回的 Promise 在 effect 函数体执行完成后才 resolve因此await之后 reducerincrement产生的状态更新一定已经完成store.getState().count可以直接断言为3。测试 effect 的“直通”路径官方文档给出的第二个用例与上面结构相同验证的是 effect 本身执行后的状态结果effect 最终委派给同 model 的 reducer 完成状态变更describe([count] model, () { it(effect: my incrementAsync effect should do something, async () { const store initRootModel({ models, }); await store.dispatch.count.incrementAsync(3); const countData store.getState().count; expect(countData).toEqual(3); }); });源码级解释为什么 store-based 方式能覆盖 effects要理解上面“await 之后状态一定更新”的前提可以看 packages/core/src/rematchStore.ts 中的createEffectsMiddlewarereturn (store) (next) (action: Action): any { if (action.type in bag.effects) { // first run reducer action if exists next(action) // then run the effect and return its result return (bag.effects as any)action.type, action.meta ) } return next(action) }即当 dispatch 的动作类型命中某个 effect 时中间件先执行 reducer 链路next(action)再调用 effect 函数本体并把返回值effect 的 Promise作为整个 dispatch 的返回值。这正是await store.dispatch.count.incrementAsync(3)可行的原因——dispatch 返回的就是 effect 的 Promise。effect 函数的入参约定也可以从该实现得到印证effect 收到的第一个参数是action.payload第二个参数是store.getState()。官方测试 packages/core/test/effects.test.ts 对此有专门的用例first param should be payloadeffect 第一个参数收到 dispatch 时传入的 payloadsecond param should contain stateeffect 第二个参数收到当前完整 state如{ count: 7 }。同一测试文件还覆盖了更多 store-based 场景例如 effect 链式调用其他 effectasyncCallAddOne内部await this.asyncAddOne()以及 通过this调用本 model 的 reducerthis.addOne()触发同 model 的 reducer这些模式与文档示例中dispatch.count.increment(payload)的写法互补可一并参考。此外store.test.ts 展示了 store 初始化层面的测试断言方式无参init()得到空 state、redux.initialState/redux.reducers/redux.rootReducers的初始状态与全局 reducer 行为、动态addModel后 state 的结构等都是可直接套用的断言范式。四、Jest 测试二脱离 Store 直接测试 Effect 函数当只想验证某个 effect 的调用逻辑而不想初始化整个 store、或需要 mock 掉它调用的 reducer时可以利用 effect 函数本身Rematch 的 effect 最终都会以 model dispatcher 作为this执行见 dispatcher.ts 中effects[name].bind(modelDispatcher)的绑定逻辑因此在测试中可以用Function.prototype.call手动注入一个“伪 dispatcher”上下文把 effect 内部要调用的 reducer 替换成jest.fn()。官方文档给出的模板如下import { count } from ./count; describe(myModel model, () { it(effect: my effectName should do something, async () { const reducerMockFn jest.fn(); // bind the functions you want to check await (count.effects as any).incrementAsync.call( { reducerThatIsGoingToBeCalled: reducerMockFn }, { payload: } ); // checking if it was called expect(reducerMockFn).toHaveBeenCalled(); // checking if it was called with the expected params expect(reducerMockFn).toHaveBeenCalledWith(something); }); });需要注意的适用前提该方式要求 effect 以对象字面量方式定义而非(dispatch) ({...})函数式因为测试直接取count.effects上挂载的原始函数对于函数式 effectseffect 闭包持有的是真实 dispatch无法这样注入 mock注入的this上下文中只放 effect 实际会用到的成员文档示例中reducerThatIsGoingToBeCalled是占位命名实际使用时应替换为 effect 内部真正调用的 reducer / 其他 effect 名断言分两层toHaveBeenCalled()验证“被调用了”toHaveBeenCalledWith(something)验证“以预期参数被调用”。从 dispatcher.ts 的createActionDispatcher还能看到每个 action dispatcher 都会被附加一个isEffect布尔标记用于区分 reducer 与 effect——这条内部细节虽不直接参与测试但解释了 Rematch 中 effect dispatcher 与 reducer dispatcher 在底层是统一的动作分发机制测试时统一通过store.dispatch[model][action]触发即可。五、Testing Library 测试连接 Rematch 的 React 组件组件级测试接近 e2e 场景先渲染组件、再模拟用户交互、最后断言 DOM。因为 Rematch 状态通过 react-redux 暴露渲染时必须用Provider包裹组件。官方文档建议封装一个测试辅助函数完整代码如下// testUtils.tsx import React from react import { render } from testing-library/react import { Provider } from react-redux import type { Store } from redux export const renderWithRematchStore (ui: React.ReactElement, store: Store) render(ui, { wrapper: ({ children }) Provider store{store}{children}/Provider, })之后在测试中用renderWithRematchStore替代testing-library/react原生的render即可渲染任何使用useSelector或useDispatch连接 Rematch store 的组件。示例组件 ButtonCounter文档假设存在一个ButtonCounter组件读取countmodel 的状态并在点击时 dispatchincrementAsynceffectimport React, { useEffect } from react import { useDispatch, useSelector } from react-redux import type { RootState, Dispatch } from ./store export const ButtonCounter () { const dispatch useDispatchDispatch() const counter useSelector((rootState: RootState) rootState.count) return ( div span aria-labelCounterCurrent counter: {counter}/span button aria-labelIncrement Button typebutton onClick{() dispatch.count.incrementAsync(1)} Increment Asynchronous /button /div ) }组件测试用例import React from react; import { screen } from testing-library/react; import { store } from ./store; import { renderWithRematchStore } from ./testUtils; import { ButtonCounter } from ./ButtonCounter; describe(ButtonCounter, () { it(should be rendered correctly, async () { renderWithRematchStore(ButtonCounter /, store); expect(screen.getByLabelText(Increment Button)).toBeInTheDocument(); await userEvent.click(screen.getByLabelText(Increment Button)); expect(screen.getByLabelText(Increment Button)).toBeInTheDocument(); expect(screen.getByLabelText(Counter)).toEqual(Current counter: 1); }); });这个用例验证了完整的用户路径点击按钮 → dispatchincrementAsynceffect → effect 内部调用incrementreducer →useSelector订阅触发重渲染 → DOM 上的Current counter: 1更新。能够稳定查询到这两个元素依赖的是组件上的aria-label属性配合 Testing Library 导出的screen对象与.toBeInTheDocument()等断言来自testing-library/jest-dom。仓库中的真实范例仓库示例工程 all-plugins-react-ts 就按这个思路组织了组件测试见 examples/all-plugins-react-ts/src/index.test.tsxconst TestingProvider: React.FC ({ children }) ( Provider store{store}{children}/Provider ) test(Application is rendered correctly, () { const { container } render(GlobalApp /, { wrapper: TestingProvider }) expect(container).toMatchSnapshot() }) test(Store is correctly initialized, () { render(GlobalApp /, { wrapper: TestingProvider }) expect(store.getState()).toEqual({ cart: { taxPercent: 8, items: [ { name: apple, value: 1.2 }, { name: orange, value: 0.95 }, ], }, // ...loading / players / settings / updated 等插件注入的状态 }) })该示例展示了两种常见策略的叠加快照测试对渲染出的 DOM 容器toMatchSnapshot()快照保存在 examples/all-plugins-react-ts/src/snapshots/index.test.tsx.snap状态测试渲染后直接断言store.getState()的完整结构其中包含loading、updated等插件注入的切片——这也说明插件会扩展 state 形状全量断言时要把插件引入的状态一并算进去。对比文档中的renderWithRematchStore封装与这里的wrapper: TestingProvider写法两者本质相同都是利用render的wrapper选项注入Provider。封装成辅助函数文档方式适合大量组件复用内联wrapper仓库示例方式则更轻量。六、测试策略速查测试目标推荐方式关键 API参考reducer 状态变更store-based Jest 用例initstore.dispatchgetState()Testing、packages/core/test/effects.test.tseffect 的端到端结果await store.dispatch.model.effect()后断言 stateeffects 中间件返回 effect PromiserematchStore.tseffect 的调用逻辑mock 依赖jest.fn()effects.fn.call(ctx, payload)手动注入伪 dispatcher 上下文Testingstore 初始化 / 初始状态 / 动态模型直接断言init结果redux.initialState、addModelpackages/core/test/store.test.tsReact 组件 storeTesting Library ProviderwrapperrenderWithRematchStore、screen.getByLabelTextexamples/all-plugins-react-ts/src/index.test.tsx几条落地建议async 用例一律awaiteffect 调用仓库通过 testsetup.js 将未处理 rejection 直接抛成测试失败可避免断言时机错误测试文件内每个用例独立init保持状态隔离组件测试优先使用aria-label等语义化属性查询避免依赖易变的选择器插件loading、persist、updated 等会向 state 中注入额外切片全量toEqual断言时参考 all-plugins-react-ts 的状态快照 把这部分也纳入预期。以上所有模式均以当前仓库rematch/core2.2.0peer 依赖redux 4的实际源码与测试套件为准可直接在任意 Jest/Mocha/Ava 项目中复制使用。赞分享前端【免费下载链接】rematchThe Redux Framework项目地址https://gitcode.com/gh_mirrors/re/rematch点击查看免费下载相关推荐使用 Jest 与 testing-library/react 测试 Gatsby React 组件使用 Jest 与 testing library/react 测试 Gatsby React 组件 导读 本文是 Gatsby 官方测试指南《Testin前端静态站点Web框架为什么选择AI-Infra-Guard腾讯朱雀实验室核心引擎技术解析为什么选择AI Infra Guard腾讯朱雀实验室核心引擎技术解析 AI Infra Guard是由腾讯朱雀实验室开发的一站式AI安全评估工具集成了Ope人工智能AI 安全治理红蓝对抗AI Agent模型安全为什么选择castero10个让终端播客更高效的理由为什么选择castero10个让终端播客更高效的理由 castero是一款专为终端打造的TUI播客客户端它将高效操作与简洁界面完美结合让你在命令行环境中也上一篇小红书下载工具 XHS-Downloader 怎么用一条链接从浏览器到硬盘的完整搬家指南下一篇QQ空间说说备份工具GetQzonehistory半小时把十年说说完整搬回本地创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
RocketRide 节点 README Schema 完全指南:让每个节点的文档与 services.json 元数据严格对齐 【免费下载链接】rocketride-server High-performance AI pipeline engine with a C core and 50 Python-extensible nodes. Build, debug, and scale LLM workflows with 13 model providers, 8 vector databases, and agent orchestration, all from your IDE. Includes VS C… · 2026/9/25 15:42:25
AI视频生成进阶:用镜头语言、构图与运镜提升出片率 1. 为什么光靠 Prompt 已经不够用了过去一年我帮十几个团队做过 AI 视频生成的工作流搭建,从广告短片到电商主图视频,踩过的坑比生成的片子还多。最开始大家的思路都差不多:把提示词写得越长越细,恨不得把每一个像素都描述出来。结… · 2026/9/25 15:42:00
HydraDB HTTPS 查询 API 教程:JSON 与 NDJSON 接口完整实战指南 HydraDB HTTPS 查询 API 教程:JSON 与 NDJSON 接口完整实战指南 【免费下载链接】hydradb HydraDB - fast graph database on object storage 项目地址: https://gitcode.com/gh_mirrors/hyd/hydradb
HydraDB 是一个构建在对象存储之上的分布式图数据库&… · 2026/9/25 15:41:42
Unity安装VS2019失败排查指南:从安装报错到编辑器关联修复 1. 为什么Unity装不上VS2019这件事值得单独拿出来说如果你在Unity里点了"Install with Unity"或者手动去装Visual Studio 2019,结果卡在下载、卡在安装、卡在"正在配置"然后弹一个没头没尾的错误码——恭喜你,你踩的是UnityVS2019这… · 2026/9/25 16:07:59
Univer:下一代开源协作办公套件,从在线表格到插件化架构 说个真事,我有段时间负责给公司搭一个在线数据处理平台,最开始图省事,直接在网页里嵌了个开源的类Excel组件,结果数据一上万行,滚动就像放幻灯片一样卡顿,更别提多人在线编辑了。后来我调研了一圈ÿ… · 2026/9/25 16:07:59
超声波气象站核心原理与STM32时差法风速测量设计 第一次接到“超声波气象站”这种项目需求的人,很容易把它想象成把一堆现成的超声波测距模块拼到一起。真正做起来你才会发现,核心不是“测距”,而是高精度的时间差测量,外加一套能在户外风吹雨打两三年不罢工的结构设计。市面上的… · 2026/9/25 16:07:47
Linux设备模型深度解析:从kobject、总线到sysfs与电源管理 网上聊 Linux 内核的文章不少,大多数都在讲调度器、内存管理、中断、RCU 锁这些“显学”。真正让我有“相见恨晚”之感的,反而是那套不怎么被渲染、默默支撑起整个驱动体系的Linux 设备模型。第一次在一台嵌入式板子上顺着 /sys 目录读设备树时ÿ… · 2026/9/25 16:07:34
ESP32纯C实现AI语音助手:无Linux无Node.js的MimiClaw实战 最近一直在折腾一个小硬件的语音助手方案,本来想直接用树莓派 Linux 起步,后来看到有人用纯 C 在 ESP32 上做了一套叫 MimiClaw 的 AI 助手,不加 Linux、不跑 Node.js,整套逻辑全部用 C 语言实现,有点颠覆我对“AI 助… · 2026/9/25 16:07:28
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37