
前端CMS【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址https://gitcode.com/gh_mirrors/wp/wp-calypso点击查看免费下载本文以 wp-calypso 官方《Unit Tests》测试指南为主线系统梳理这个 WordPress.com 前端单体仓库Calypso的单元测试技术栈、运行命令、测试目录约定与断言/模拟最佳实践并结合仓库内的 Jest 预设、实际测试用例与配置源码逐层印证。读完你将掌握如何在 Calypso以及采用同一套automattic/calypso-jest预设的包中编写、运行、维护单元测试并理解其背后的工程约束。为什么写单元测试测试即文档测试即代码Calypso 的测试指南开宗明义单元测试的价值不只在于“保证应用行为符合预期”更在于它们为如何使用一段代码提供了最精简的示例。一个写得很好的测试本身就是一份可执行的使用文档。与此同时测试代码也是代码库的一部分unit-tests.md 明确要求对测试代码应用与业务代码完全一致的代码标准——它们同样需要被维护、被 review、被重构。需要注意的平衡点是为写测试而写测试不是目标。正确目标是在“覆盖预期与非预期行为”“执行速度”和“代码可维护性”之间找到平衡。单元测试应当测试行为的单元units of behavior包含尽量少的抽象层。如果测试经常因合理的业务变更而失败往往说明测试与代码内部实现耦合过紧测试反方差现象见文末推荐阅读中的《Test Contra-variance》。动手写测试前指南建议先自问四个问题我们在测试哪些行为运行这段代码时可能出现哪些错误测试真的在测我们认为它在测的东西吗会不会引入假阳性/假阴性它可读吗其他贡献者能否只通过阅读对应测试就理解代码的行为技术栈与测试基建Jest Testing Library calypso-jest 预设当前 Calypso 的单元测试统一使用 Jest。虽然代码库中仍能见到 Chai 断言和 Sinon spy/mock 的历史遗留写法但所有新测试都应使用 Jest 的全局 APIdescribe、test、beforeEach等、断言expect、mock、spy 与 mock 函数。仓库为这套约定提供了开箱即用的基建automattic/calypso-jest包导出一个 Jest 预设presetjest-preset.js 的核心配置如下module.exports { resolver: require.resolve( ./src/module-resolver.js ), setupFilesAfterEnv: [ require.resolve( ./src/setup.js ) ], testEnvironment: node, testMatch: [ rootDir/**/test/*.[jt]s?(x), !**/.eslintrc.* ], transform: { \\.(?:[jt]sx?|mjs)$: [ babel-jest, { rootMode: upward } ], \\.(gif|jpg|jpeg|png|svg|webp|scss|mp4|sass|css)$: require.resolve( ./src/asset-transform.js ), }, // ... };从中可以看到几条关键工程决策默认测试环境是node以换取更快的执行速度需要 DOM 的测试必须显式声明 jsdom 环境下文详述自动发现测试testMatch只匹配rootDir/**/test/*.[jt]s?(x)即各工作目录下test文件夹中的.js/.ts/.jsx/.tsx文件与文档“把测试放进test子文件夹”的约定完全一致资源文件转译.svg/.scss/.png等非 JS 资源由asset-transform.js转成返回文件 basename 的模块避免 Jest 解析失败详见 calypso-jest/README.md 中的国旗 SVG 示例模块解析module-resolver.js 在解析时注入calypso:src条件导出并针对parsel-js这类同时命中browser条件导致 CJS/ESM 混淆的包做了特殊处理全局环境补齐setup.js 为wordpress/componentsmock 了global.CSS.supports并在 React 19 的react-dom/server.browser与scheduler引用MessageChannel时提供一个纯 JS 的迷你实现避免 worker_threads 的端口句柄导致 Jest worker 无法优雅退出。客户端测试在此基础上进一步定制test/client/jest.config.js 使用projects把套件拆成client与dashboard两个 displayName设置rootDir为client、缓存目录为.cache/jest并通过moduleNameMapper把automattic/calypso-config映射到rootDir/server/config/index.jssetup-test-framework.js 则引入testing-library/jest-dom、用nock.disableNetConnect()在测试期间全局禁用所有网络请求并补齐TextEncoder/TextDecoder、ResizeObserver、IntersectionObserver等 jsdom 未实现的对象。运行测试client / server / packages / integration 四套命令unit-tests.md 将运行方式的细节指向 testing-overview.md。当前仓库根目录 package.json 中实际注册的脚本节选如下# 客户端单元 组件测试验证 client 目录下代码 yarn run test-client # 监听模式默认只跑被修改文件相关的测试 yarn run test-client:watch # 服务端单元测试验证 server 目录下代码 yarn run test-server yarn run test-server:watch # 集成测试验证 bin / client / server / test 下代码如何协同工作 yarn run test-integration # 包级测试验证 packages 下的独立包 yarn run test-packages例如test-client的实际命令是TZUTC jest -ctest/client/jest.config.js——固定 UTC 时区以避免测试因本地时区不同而不稳定。这些套件具备自动测试发现能力只要把测试文件放进待测代码旁的test子文件夹即可被拾取。client/server 套件在每次 push 时由 CITeamCity执行且网络连接被禁用nock.disableNetConnect()因此单个测试必须“飞快”集成测试则允许网络与高内存处理运行时间可更长按天在 CI 上执行。E2E 测试单独维护在 test/e2e 目录详见 testing-overview.md。由于改动往往波及应用其他部分文档建议在提交前在本地完整跑一遍单元测试而不只跑自己改动的文件。目录结构test 文件夹 同名文件 mocks/fixtures 子目录测试文件必须放在工作目录下的test文件夹中且与被测文件同名-- test | -- bar.js -- bar.jsx只有至少包含一个测试用例的测试文件才能直接放在/test下如果需要外部 mock 或 fixture请放到子目录里保持整洁test/mocks/[file-name].jstest/fixtures/[file-name].js这与 Jest 预设中的testMatch: [ rootDir/**/test/*.[jt]s?(x) ]一一对应——只有test目录顶层的测试文件会被发现mocks/fixtures 子目录天然不会被误识别为测试用例。导入测试优先相对路径基于上述目录结构导入被测代码时优先使用相对路径而不是项目别名路径推荐import { bar } from ../bar;不推荐import { bar } from components/foo/bar;理由很实际一旦把代码迁移到应用目录的其他位置相对导入的测试无需修改即可继续工作。值得补充的是相对路径是“导入被测代码”的推荐写法而仓库大量测试在 mock 依赖时反而使用calypso/...别名见下文jest.mock( calypso/lib/wp, ... )因为 mock 的对象就是要精确指向全局唯一的那份依赖。描述测试describe 分组 行为化命名用describe块对测试用例分组每个测试用例只描述一种行为。命名时用自然语言描述预期行为对 UI 组件而言最好从用户视角描述而不是解释代码内部实现好describe( CheckboxWithLabel, () { test( checking checkbox should disable the form submit button, () { /*...*/ } ); } );不好describe( CheckboxWithLabel, () { test( checking checkbox should set this.state.disableButton to true, () { /*...*/ } ); } );后者把测试与state.disableButton这个内部实现细节绑死如果重构改掉内部字段名即使按钮禁用行为完全没变测试也会无辜地失败——这正是“测试过度耦合内部实现”的典型信号。快照测试守护大型数据结构与组件结构快照测试用于验证测试过程中产生的任何数据没有被无意改变尤其适合组件树、state 树这类大型复杂结构。完整规范见 snapshot-testing.md。一个快照极易生成test( foobar test, () { const foobar { foo: bar }; expect( foobar ).toMatchSnapshot(); } );生成出的快照文件内容大致如下永远不要手工编辑快照它们由测试生成exports[ test foobar test 1 ] Object { foo: bar, } ;快照的优缺点与适用场景优点写测试琐碎简洁防止无意变更易于协作无需运行应用即可揭示内部结构缺点不具表达力只能在引入变更时发现问题对任何非确定性内容随机数、时间、依赖外部响应都很棘手主要场景组件测试与reducer 测试。Calypso 最早的一批快照正是 reducer 测试例如 client/state/comments/test/selectors.js 中直接对getPostCommentsTree的选择器结果做快照第 152–165 行describe( #getPostCommentsTree, () { test( should return the tree structure, () { const tree getPostCommentsTree( state, 1, 1, all ); expect( tree ).toMatchSnapshot(); } ); test( should reverse children, () { expect( getPostCommentsTree( stateWithDeeperChildren, 1, 1, all ) ).toMatchSnapshot(); } ); // ... } );Reducers 产生的是“我们不想意外变更”的大型复杂数据结构正是快照的强项。更新快照的三种方式当快照测试失败通常意味着组件渲染发生了改变。若变更是有意的按以下步骤更新快照# --testPathPattern 可选但能只跑匹配的测试、大幅提速 yarn run test-client -- --updateSnapshot --testPathPattern client/components随后review diff确认变更符合预期且有意为之再提交。日常开发中官方建议把yarn run test-client:watch挂在后台Jest 只会运行被改动文件相关的测试快照失败时按u即可就地更新快照。另外要注意**连接组件connected components**的快照被connect()包裹的组件很难直接快照最佳实践是把未连接组件一并导出// my-component.js export { MyComponent }; export default connect( mapStateToProps )( MyComponent ); // test/my-component.js import { MyComponent } from ..; // 在这里运行 MyComponent 的测试……连接所需的 props 需要手工提供——这反而是审计连接 state 的好机会。快照本身不表达任何“期望”因此最好与真正描述期望的断言搭配使用例如先expect( container ).toMatchSnapshot()捕获无意变更再用expect( screen.getByText( Also available in ) ).toBeVisible()表达真正的测试意图。Setup 与 Teardown测试生命周期的正确姿势Jest 提供了一组 setup/teardown 方法可以在每个测试、所有测试或某个describe块内的测试前后执行任务。它们支持异步代码——像单测用例一样返回 Promise 即可让 Jest 等待其 resolve// 所有测试执行前的一次性 setup beforeAll( () someAsyncAction().then( ( resp ) { window.someGlobal resp; } ) ); // 所有测试执行后的一次性 teardown afterAll( () { window.someGlobal null; } );afterEach与afterAll是“测试后清理”例如重置 state 数据的首选方式。切记不要把清理代码放在断言之后——一旦前面的断言失败清理不会执行可能连累无关测试一起失败。模拟依赖从依赖注入到 jest.mock方式一依赖注入Dependency Injection把依赖作为参数传给函数往往能让代码更易测试应尽量避免在更高作用域引用依赖。不好依赖被硬编码进函数体内import VALID_VALUES_LIST from ./constants; function isValueValid( value ) { return VALID_VALUES_LIST.includes( value ); }这时要测试就必须导入并使用VALID_VALUES_LIST的值expect( isValueValid( VALID_VALUES_LIST[ 0 ] ) ).toBe( true );这个断言同时测了两件事1) 函数能识别列表中的项2) 它能识别VALID_VALUES_LIST中的项。可如果测试者根本不关心列表里存了什么——例如列表来自 HTTP 请求——就只想验证“函数能否识别列表中的项”呢好依赖作为参数传入function isValueValid( value, validValuesList [] ) { return validValuesList.includes( value ); }把列表作为参数传入后测试里就能传 mock 数据还能顺带覆盖更多场景expect( isValueValid( hulk, [ batman, superman ] ) ).toBe( false );expect( isValueValid( hulk, null ) ).toBe( false );expect( isValueValid( hulk, [] ) ).toBe( false );expect( isValueValid( hulk, [ iron man, hulk ] ) ).toBe( true );方式二jest.mock桩掉导入的依赖当外部/内部库的方法和属性在多处被使用把参数传来传去就变得笨拙且不现实。此时jest.mock是优雅的替代方案。Calypso 的典型场景是config模块——通过 feature flag 控制大量功能对应包为automattic/calypso-config。假设被测模块// bilbo.js import config from automattic/calypso-config; export const isBilboVisible () ( config.isEnabled( the-ring ) ? false : true );测试时桩掉 config 对象并用 jest mock 函数控制isEnabled的返回值// test/bilbo.js import { isEnabled } from automattic/calypso-config; import { isBilboVisible } from ../bilbo; jest.mock( config, () ( { // bilbo 默认可见 isEnabled: jest.fn( () false ), } ) ); describe( The bilbo module, () { test( bilbo should be visible by default, () { expect( isBilboVisible() ).toBe( true ); } ); test( bilbo should be invisible when the the-ring config feature flag is enabled, () { isEnabled.mockImplementationOnce( ( name ) name the-ring ); expect( isBilboVisible() ).toBe( false ); } ); } );mockImplementationOnce让测试可以按用例精确控制某一次调用的返回值从而在同一个 mock 上覆盖多个 feature flag 分支。在真实配置层面test/client/jest.config.js 通过moduleNameMapper把automattic/calypso-config映射到client/server/config/index.js保证测试环境下 feature flag 解析走的是真实配置实现。测试全局对象jsdom 环境 Jest spy用 Jest spiesjest.spyOn可以测试调用全局方法的代码。桩掉全局作用域的 DOM 属性或方法时务必在文件头部加jest-environment jsdom注释确保存在可供桩的 DOM/** * jest-environment jsdom */ import { myModuleFunctionThatOpensANewWindow } from ../my-module; describe( my module, () { beforeAll( () { jest.spyOn( global, open ).mockImplementation( () true ); } ); test( something, () { myModuleFunctionThatOpensANewWindow(); expect( global.open ).toHaveBeenCalled(); } ); } );这里jest-environment jsdom正好呼应了 jest-preset 的默认testEnvironment: node决策绝大多数纯逻辑测试不需要 DOM跑在 node 环境下更快只有确实需要浏览器对象window、document、global.open等的测试才显式切换到 jsdom。仓库中大量组件测试文件都以该 docblock 开头例如 client/components/themes-list/test/index.jsx 第一行就是/** jest-environment jsdom */。测试遗留代码为 lib/wp 这类老接口写桩Calypso 是一个不断演进的庞大代码库你难免会遇到需要扩展或修改遗留代码测试的情况。当前团队处理远程/异步数据获取的首选方案是 data layer但代码库中仍可能见到组件或 action 直接调用 client/lib/wp该模块提供共享的WPCOMAPI 实例用于与 WordPress.com REST API 交互详见 client/lib/wp/README.mdwpcom.someWpComMethod().then( ( error, data ) { // do something with the response } );这种场景同样可以用 Jest 桩掉整个库// jest.mock( lib/wp, () ( { someWpComMethod: jest.fn(), } ) );仓库里可以看到大量同类实战写法例如 client/my-sites/earn/memberships/test/products-list.tsx 中被测组件通过import wpcom from calypso/lib/wp发起 API 调用测试则整体替换jest.mock( calypso/lib/wp, () ( { __esModule: true, default: { req: { post: jest.fn() } }, } ) );随后测试在用例内用jest.fn()的返回值控制接口响应验证删除计划弹窗等交互行为。这正是“外部依赖用 mock、被测代码走真实实现”的典型组合。组件测试与 HOC 陷阱测试未包裹组件组件测试的完整方法论见 component-tests.md这里只强调与单元测试约定直接相关的两个关键点浅渲染shallow rendering只渲染一层组件、不渲染子组件用于断言 render 方法返回的结构。不过需要注意随着 Testing Library 取代 Enzyme仓库当前的推荐做法是“尽量贴近用户真实体验”来测试而不是只做浅层断言。以 client/components/themes-list/test/index.jsx 为例它渲染ThemesList后用screen.getAllByTestId( /theme-/ )断言每个 theme 都渲染出来同时用jest.mock( calypso/components/theme, ... )把 Theme 子组件桩成轻量占位从而把测试隔离在目标组件内部——这正是“最小化调用目标代码之外代码”的单元测试原则。HOC 陷阱如果组件被localize()或connect()这类高阶组件包裹直接渲染往往只能得到外层 HOC。最佳实践是导出未包裹的组件用于测试外部依赖用 mock 隔离// Bad. 测试无法访问未包裹的组件。 export default localize( class SomeComponent extends React.Component { // ... } );// Good! 这个组件可以被导入用于测试。 export class SomeComponent extends React.Component { // ... } // 默认导出包裹后的组件供其他地方使用。 export default localize( SomeComponent );仓库实现正是如此client/components/themes-list/index.jsx 第 66 行导出export const ThemesList未包裹组件测试直接导入文件末尾才用export default connect( ... )( localize( withIsFSEActive( ThemesList ) ) )导出默认的包裹版本第 484–487 行。组件交互与渲染断言补充速览虽然本篇聚焦单元测试但组件测试与它共享同一套 Jest Testing Library 生态。几个常见断言模式值得记录完整见 component-tests.md渲染结果与子组件const { container } render( MyComponent / );后expect( screen.getByRole( dialog ) ).toBeVisible();Props 传递expect( wrapper.find( AnExpectedChildComponent ).props( fantastic ) ).toBe( true );类方法expect( MyComponent.prototype.appendWorldBang( Hello, ) ).toBe( Hello, world! );或wrapper.instance().shouldShowPlaceholder()交互处理配合testing-library/user-eventimport { render, screen } from testing-library/react; import userEvent from testing-library/user-event; test( should remove an item when its Remove button is clicked, async () { const user userEvent.setup(); render( MyComponent / ); await user.click( screen.getAllByRole( button, { name: Remove } )[ 0 ] ); const field screen.getByRole( textbox, { name: Your Field } ); expect( field ).toHaveValue( bar ); } );另外Calypso 历史上曾通过automattic/calypso-jest提供 Enzyme 支持但由于 Enzyme 与 React 18 不兼容阻碍 React 18 升级已弃用 Enzyme全面转向testing-library/react——原因包括它能写出更具可访问性的测试、更贴近用户真实体验。若外部项目仍想使用 Enzyme可手动安装enzymewojtekmaj/enzyme-adapter-react-17并在setupFilesAfterEnv中配置适配器详见 component-tests.md 的配置示例。进一步阅读testing-overview.mdclient/server/integration/e2e 各套件的完整运行方式与 CI 策略component-tests.mdReact 组件测试的渲染、交互、浅渲染与 HOC 陷阱snapshot-testing.md快照测试的完整规范、痛点与最佳实践packages/calypso-jest/README.mdautomattic/calypso-jest预设的用法与资源转译器test/client/jest.config.js 与 test/client/setup-test-framework.js客户端测试的真实配置与全局测试框架client/components/themes-list/test/index.jsx 与 client/state/comments/test/selectors.js组件测试与 reducer 快照测试的仓库内范例编写测试时请始终把《Test Contra-variance》与《The Failures of Intro to TDD》两篇文章的观点放在心上测试应当与代码的对外行为对齐而非与内部实现对齐——这正是本指南所有约定背后的统一原则。赞分享前端CMS【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址https://gitcode.com/gh_mirrors/wp/wp-calypso点击查看免费下载相关推荐ComfyUI 单元测试指南基于 pytest 的测试依赖安装、运行与目录组织ComfyUI 单元测试指南基于 pytest 的测试依赖安装、运行与目录组织 ComfyUI 是一个基于图/节点界面的模块化扩散模型 GUI 与后端引擎其人工智能大模型媒体生成本地部署AppIntro单元测试模拟使用Mockito模拟依赖组件的测试策略AppIntro单元测试模拟使用Mockito模拟依赖组件的测试策略 单元测试是确保AppIntro组件稳定性的关键环节尤其是在处理复杂依赖关系时。本文将通移动开发UI组件bark!完全部署教程从Pipewire配置到多设备同步播放bark!完全部署教程从Pipewire配置到多设备同步播放 bark是一款专为本地网络设计的实时同步音频流工具通过UDP multicast技术实现低延迟上一篇终端复制总是乱码claude-devtools 一键复制粘贴与会话导出 Markdown/JSON 完全指南下一篇Xonsh 内置事件系统Built-in Events完全指南从命令生命周期钩子到子进程环境掩码实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考