新闻详情

新闻详情

首页 / 资讯中心 / 详情

单元测试从入门到实战:覆盖Vue与嵌入式框架选型、Mock与排错全攻略

发布时间:2026/10/1 14:52:20来源:尧图网络
单元测试从入门到实战:覆盖Vue与嵌入式框架选型、Mock与排错全攻略
接手过不少被测试代码追着跑的项目之后我对单元测试的态度从交差变成了救命。团队里很多人一开始问我单元测试到底怎么下笔尤其是面前摆着 Vue 全家桶、或者嵌入式 C 代码、又或者公司强制要求的 Testbed 工具时往往一脸懵。有人说这不是写几个断言跑一下的事吗真上手后才发现坑多得很。这篇算是我这几年的压箱底整理从框架选型到 Vue 项目实战再到嵌入式 C 的 Testbed 流程最后是高频报错的完整排查思路尽量做到够详细、够直接。1. 动手之前先搞清楚单元测试到底在测什么很多人对单元测试的误解从第一句就开始了单元测试不是程序跑通了没而是当输入满足特定条件时这个函数、组件、模块的输出是否符合约定。这俩看着相近实际差着一条鸿沟。1.1 单元测试的边界测的是逻辑不是系统单元测试的测法是隔离——把代码拆到最小的可独立运行的单元通常是函数、类、组件然后单独验证它。它不关心数据库里有没有数据不关心后端是否返回 200不关心网络通不通也不关心 UI 上点了按钮之后整个页面跳转对不对。那些属于集成测试、E2E 测试的范畴。我经常用一个例子说明单元测试formatPrice(1234.5)传入参数后返回了1,234.50字符串对不对集成测试调用订单服务后确认金额数据流经转化函数后能正常写入数据库。E2E 测试用户打开下单页输入金额点击按钮看到最终的格式化结果。所以很多人项目里明明写了单元测试却把路由跳转、HTTP 请求、localStorage 读写全塞进一个单测里跑一次慢如蜗牛还动不动因为环境影响挂掉。这不是单元测试这是拿单元测试的筐装集成测试的西瓜。1.2 好测试的四条硬标准我自己判断一个测试写得好不好不看它数量多不多而是看四条快一个测试跑完应该在毫秒级跑完整个单测集合理想情况不超过几分钟。如果出现 sleep、网络超时、磁盘 IO 等待那一定不是单元测试。隔离测试之间互相独立跑的顺序打乱、单跑一个用例或全量跑结果一致。确定性同一份代码同一份用例跑一百次结果都一样。任何随机数、时间戳、环境差异都要想办法固定或注入。可读性测试代码本身就是文档。三个月后你回来看测试应该能一秒读懂被测单元的预期行为而不是得像破案一样猜。这四条里最容易被忽略的是确定性。我在实际项目里见过太多的测试是偶尔挂一次用户一反馈第一反应是重跑一下看它过没过。这种测试早晚失去团队信任最后沦为 CI 里的摆设。1.3 覆盖率不是 KPI但也不是没用代码覆盖率常被当成流程指标。XX% 以下不让合入分支、不允许发版。覆盖率有参考价值但它衡量的是哪些代码被执行了不是哪些行为被验证了。if (a 0) { return 1 } else { return -1 }只有一行测试只覆盖a 0时行覆盖率 100%但分支覆盖率 50%。真正危险的往往不是没执行到的行而是没验证到的行为路径。所以我建议团队定指标时不要只看行覆盖至少同时看分支覆盖或函数覆盖。覆盖率可以设门槛但不能当最终考核标准。宁可少写几个用例也要把关键分支和异常分支补全。2. 测试框架选型不同技术栈的答案完全不一样选框架这事儿只看热度和别人推荐没用。同一个团队里前端、后端、嵌入式三个方向可能要用三套完全不同的方案。我见过因为前端项目用了 Jest后端的同事也硬搬过来测 C 代码的例子那真是互相折磨。2.1 前端 Vue 项目Vitest、Jest、Mocha 怎么选先说结论Vue 3 Vite 项目我无脑推荐 Vitest。Vitest 和 Vite 共享同一套配置体系跑测试时不需要再起一个单独的编译流程速度明显快过 Jest。尤其是碰到大量组件的项目这个速度差非常直观。Jest 仍然是个好框架但它和 Vite 项目结合时要面对的额外配置太多了ESM 转换、路径别名解析、Vue SFC 编译、tsconfig 映射每一样都是时间黑洞。如果你的项目还在用 Vue CLI那 Jest 是老搭档稳定成熟。但新项目只要用了 Vite省心路线就是 Vitest。Mocha 我不太推荐作为主框架它太裸了断言库要另装 ChaiMock 要另装 Sinonspy、stub、mock 三个概念分别在两个库里维护心智负担重。它更像一个底层执行器适合做二次封装不适合直接当团队的测试框架。2.2 后端与跨语言场景JUnit 全家桶与 pytestJava 后端不用想JUnit 5 Mockito或 MockK 处理 Kotlin基本是事实标准。Spring Boot 项目里还可以配合spring-boot-starter-test它把 JUnit、AssertJ、Mockito、Hamcrest 全打进去了。Python 这边 pytest 是更现代、更好用的选择。原生 unittest 能干活但写起来啰嗦fixture 机制、参数化、插件生态都比不上 pytest。不过说实话如果你的团队没什么历史包袱直接从 pytest 上手就行。2.3 专业嵌入式工具Testbed 和轻量方案对比嵌入式这块比较特殊。如果公司流程过了 CMMI 或汽车行业功能安全认证基本绕不开 Testbed。Testbed原 LDRA Testbed是专业测试工具能对 C/C/Ada 代码做静态分析、复杂度分析、覆盖率分析、基于插桩的动态测试还能自动生成测试用例的驱动程序是很多军工、汽车、轨交行业的标配。但 Testbed 不是唯一选择。如果只是想让嵌入式代码有基本的单测保障轻量方案也能用在 PC 上用 GCC 编译测试文件配合一个轻量的断言库或手写简单断言宏对被测函数直接做白盒测试。这种方式胜在零成本、启动快、极易落地适合中小团队或早期验证。Testbed 的好处是体系完整、报告规范、认证友好坏处是价格贵、学习成本高、用例管理偏重。选哪个看你项目的质量和合规要求。为了方便对比我把常用方案的适用场景列一张表技术栈推荐框架适用场景不足Vue 3 ViteVitest新项目、组件与组合式函数测试生态相对 Jest 略小Vue CLI / 老项目Jest存量项目、已复用 Jest 生态与 Vite 混用配置麻烦JavaJUnit 5 MockitoSpring Boot 等后端多线程/异步测试需要额外处理Pythonpytest脚本、服务端、算法大型项目合理组织 fixture 需要经验C/C 嵌入式Testbed 或轻量断言宏汽车/军工/工业或无工具依赖验证Testbed 成本高轻量方案报告不完整3. 从零写第一个有说服力的测试断言、Mock 与参数化框架只是皮真正决定测试价值的是用例怎么设计。选好框架之后得先掌握最核心的三个动作写断言、做隔离、用参数化覆盖分支。3.1 先从一个纯函数练手断言怎么写才有意义拿最常见的业务函数来演示比如一个计算订单折扣的函数。代码用 TypeScript 写// src/utils/order.ts export function calcDiscount(price: number, vipLevel: normal | silver | gold): number { if (price 0) throw new Error(price must be positive) const discountMap { normal: 1, silver: 0.95, gold: 0.88 } return Number((price * discountMap[vipLevel]).toFixed(2)) }对应的测试// src/utils/order.test.ts import { describe, it, expect } from vitest import { calcDiscount } from ./order describe(calcDiscount, () { it(普通用户不打折, () { expect(calcDiscount(100, normal)).toBe(100) }) it(银卡用户享受95折, () { expect(calcDiscount(100, silver)).toBe(95) }) it(金卡用户享受88折, () { expect(calcDiscount(100, gold)).toBe(88) }) it(价格为负数时抛出异常, () { expect(() calcDiscount(-1, normal)).toThrow(price must be positive) }) it(价格小数点后的金额按两位舍入, () { expect(calcDiscount(10.456, silver)).toBe(9.93) }) })这里有说服力的关键是正常路径、边界路径、异常路径都覆盖了。断言的是对外行为的精确结果不是内部实现。读测试的人能一眼看出函数要满足的规格。如果只是写expect(calcDiscount(100, gold)).not.toBeUndefined()这种测试跑了等于没跑。顺便说一个常见坑浮点数断言。如果你在断言里写expect(a b).toBe(0.3)而实际是0.30000000000000004测试就会莫名奇妙挂掉。JS 的浮点运算天然有精度问题建议比较金额时用toBeCloseTo(9.93, 2)或者直接在业务函数里做toFixed后再用于展示写进数据库也是存字符串或整数分。3.2 Mock、Stub 与 Spy隔离外部依赖的唯一手段单元测试最核心的难点不是写断言而是让被测代码别碰真实的外部依赖。比如前端组件里调用了axios后端服务里访问了数据库嵌入式代码读了一个 ADC 寄存器。测试环境里不能真发请求、不能真连库、不能真等硬件就绪。这里要分清三个概念Stub桩替换依赖实现固定返回预定义数据。比如把fetchUser()替换成固定返回{ name: 张三 }的函数。Spy间谍包一层监听原函数有没有被调用、被调了几次、传了什么参数可以通过vi.spyOn实现。Mock模拟不仅替换实现还能验证行为。在 Vitest 中用vi.mock()可以整体模拟一个模块。拿 Vivitest 举例最常见的 mock 外部模块方式// src/api/user.ts export const fetchUser async (id: string) { const res await fetch(/api/users/${id}) return res.json() } // src/stores/user.ts 里的某个 action import { fetchUser } from /api/user export const useUserStore defineStore(user, { actions: { async loadUser(id: string) { this.user await fetchUser(id) } } }) // 测试里只 mock 掉 fetchUser import { vi } from vitest vi.mock(/api/user, () ({ fetchUser: vi.fn() }))Mock 的原则是我只 mock 自己不拥有的东西——网络、时间、随机数、系统时钟、文件系统、第三方 SDK。自己写的纯函数尽量不要 mockmock 自己的逻辑会让测试失去意义最后变成用自己的假数据验证自己的假实现就是自欺欺人。3.3 参数化测试用一张表跑完所有分支写测试最怕的不是多而是重复。同一个函数的不同输入输出如果挨个复制粘贴用例维护起来极度恶心。Vitest 和 Jest 都支持参数化import { describe, it, expect } from vitest import { calcDiscount } from ./order describe(calcDiscount 参数化测试, () { it.each([ [100, normal, 100], [100, silver, 95], [100, gold, 88], [10.456, silver, 9.93], [0, normal, 0], ] as const)(calcDiscount(%i, %s) 应该返回 %i, (price, level, expected) { expect(calcDiscount(price, level)).toBe(expected) }) })it.each里的每一行就是一组输入输出。以后价格策略变了、折扣率变了只改数据行就行不用新增一坨用例代码。这也是测试代码可读性的体现——你要表达的是规格不是一堆 if else。4. Vue 项目里的单元测试组件、Router、Pinia 一起上很多 Vue 新手写单测跑起来第一步就挂在环境上。这里只谈 Vitest 路线整套配置直接抄即可再把 Router 和 Pinia 的测法讲透。4.1 一套能直接落地的最小 Vite Vitest 配置要在 Vue 3 项目里跑组件测试光装vitest不够还需要vue/test-utils和jsdom。安装命令示意npm install -D vitest vue/test-utils jsdom然后是vitest.config.tsimport { defineConfig } from vitest/config import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)), }, }, test: { environment: jsdom, globals: true, setupFiles: [./src/setupTests.ts], coverage: { provider: v8, include: [src/**/*.{ts,vue}], exclude: [src/main.ts, src/env.d.ts], }, }, })必须手动做两件事设置environment: jsdom否则组件里的document、window全是不存在的设置globals: true并配合tsconfig.json里types: [vitest/globals]否则每个文件都要显式import { describe, it, expect } from vitest极烦。测试环境里我还会补一个简单的 setup 文件用来 mock 掉window.matchMedia、ResizeObserver这类浏览器原生 API因为 jsdom 不支持它们而组件又在初始化时调用了它们// src/setupTests.ts import { vi } from vitest Object.defineProperty(window, matchMedia, { writable: true, value: vi.fn().mockImplementation(query ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), }) class ResizeObserverMock { observe() {} unobserve() {} disconnect() {} } window.ResizeObserver ResizeObserverMock4.2 组件测试挂载、触发、断言组件测试我现在几乎不用shallowMount除非被测组件枝条实在太长、子组件干扰太严重。大多数场景下mount更贴近真实断言更可靠。先看一个计数器组件!-- src/components/Counter.vue -- template div classcounter p>// src/components/Counter.test.ts import { mount } from vue/test-utils import Counter from ./Counter.vue describe(Counter, () { it(初始渲染为 0, () { const wrapper mount(Counter) expect(wrapper.get([data-testcount]).text()).toBe(0) }) it(点击按钮后 count 变为 1, async () { const wrapper mount(Counter) await wrapper.get([data-testincrement]).trigger(click) expect(wrapper.get([data-testcount]).text()).toBe(1) }) })两个细节值得注意第一await不能省。trigger返回的是一个 PromiseVue 的 DOM 更新是异步的。不await就立刻断言拿到的还是旧视图。这是新人最常见的测试写对了但跑不过的原因之一。第二测试的选择器。我习惯用>// src/router/index.ts import { createRouter, createMemoryHistory } from vue-router const routes [ { path: /, name: home, component: { template: divHome/div } }, { path: /detail/:id, name: detail, component: { template: divDetail/div } }, ] export const router createRouter({ history: createMemoryHistory(), routes, })测试里操作跳转// src/router/router.test.ts import { describe, it, expect } from vitest import { router } from ./index describe(router 基础跳转, () { it(跳转详情页后 currentRoute 匹配正确, async () { router.push({ name: detail, params: { id: 123 } }) await router.isReady() expect(router.currentRoute.value.params.id).toBe(123) expect(router.currentRoute.value.name).toBe(detail) }) })如果是测试组件内部调用了router.push可以用vi.mock(vue-router)把useRouter全局 mock 掉vi.mock(vue-router, () ({ useRouter: () ({ push: vi.fn(), }), useRoute: () ({ params: { id: 123 }, }), }))4.4 Pinia 测试仓库内部逻辑与组件绑定分开测Pinia 官方推荐单测时先setActivePinia(createPinia())再获取 store 实例直接调用 action。这样测的是 store 自身的逻辑跟组件一点关系都没有。// src/stores/user.test.ts import { setActivePinia, createPinia } from pinia import { beforeEach, describe, it, expect, vi } from vitest import { useUserStore } from ./user import { loginApi } from /api/auth vi.mock(/api/auth, () ({ loginApi: vi.fn(), })) describe(user store, () { beforeEach(() { setActivePinia(createPinia()) localStorage.clear() }) it(login 成功后保存 token 与用户信息, async () { vi.mocked(loginApi).mockResolvedValue({ token: token-123, user: { name: 张三 } }) const store useUserStore() await store.login(zhangsan, password) expect(store.token).toBe(token-123) expect(localStorage.getItem(token)).toBe(token-123) }) it(login 失败时不写 token, async () { vi.mocked(loginApi).mockRejectedValue(new Error(invalid password)) const store useUserStore() await expect(store.login(zhangsan, wrong)).rejects.toThrow(invalid password) expect(store.token).toBe() }) })4.5 ESLint 与 Prettier 怎么配合 Vitest 不吵架热词里有 eslint prettier vitest单元测试这个组合确实容易在工程化阶段反复折腾。最常碰到的问题有两个一是 ESLint 报describe is not defined或it is not defined。这是 ESLint 不认识globals。解决方式是前台在.eslintrc.cjs里加module.exports { root: true, env: { node: true, es2022: true, }, extends: [ eslint:recommended, plugin:vue/vue3-recommended, plugin:vitest/recommended, prettier, ], globals: { describe: readonly, it: readonly, expect: readonly, vi: readonly, }, }二是 Prettier 对测试文件中长链式断言的缩进有不同偏好最常见的是expect(...).toHaveBeenCalledWith(...)超过行长后自动换行导致与团队格式不一致。这种问题不需要在 ESLint 里写复杂规则统一交给eslint-config-prettier关闭所有与 Prettier 冲突的规则就好别再让 ESLint 管格式它只管逻辑。5. 嵌入式软件单元测试怎么做从轻量方案到 Testbed 全流程嵌入式软件的单测一直是很多团队头痛的点。没有操作系统、依赖硬件寄存器、交叉编译链复杂很多人一听就头大。但嵌入式 C 代码其实非常适合做单元测试——因为函数大多接受参数、返回结果只要你把硬件依赖挡在外面测试起来比 UI 组件还简单。5.1 嵌入式单测的核心难点与桩函数设计嵌入式代码和普通 C 代码最大的区别是被测函数经常直接调用 HAL 函数、寄存器宏、外部全局变量。比如一个read_temperature()函数内部可能调用了HAL_ADC_Read()测试环境里根本没这块硬件。思路很朴素写一个同名桩函数替换掉它让桩函数返回一个你在测试里可控的数据。/* src/adc.h */ #ifndef ADC_H #define ADC_H #include stdint.h int16_t HAL_ADC_Read(void); #endif /* src/temperature.c */ #include temperature.h float convert_temperature(void) { int16_t adc_value HAL_ADC_Read(); /* 假设温度 ADC值 * 0.01 度 */ return adc_value * 0.01f; }测试文件里你不想真的链接 HAL 库就自己定义一个同名桩/* tests/stubs/stub_adc.c */ int16_t fake_adc_value 0; int16_t HAL_ADC_Read(void) { return fake_adc_value; }然后测试函数里直接改fake_adc_value来模拟不同的 ADC 采样值/* tests/test_temperature.c */ #include minunit.h #include temperature.h extern int16_t fake_adc_value; MU_TEST(test_convert_temperature_at_zero) { fake_adc_value 0; mu_assert(convert_temperature() 0.0f, 0 度时转换错误); } MU_TEST(test_convert_temperature_at_25c) { fake_adc_value 2500; mu_assert(convert_temperature() 25.0f, 25 度时转换错误); } MU_SUITE_CONST(suite_temperature) { MU_RUN_TEST(test_convert_temperature_at_zero); MU_RUN_TEST(test_convert_temperature_at_25c); } int main(void) { MU_RUN_SUITE(suite_temperature); MU_REPORT(); return MU_EXIT_CODE; }这个示例用的minunit是个单头文件的极简测试库只要把minunit.h放进测试目录直接用gcc tests/test_temperature.c src/temperature.c tests/stubs/stub_adc.c -I src -I tests -lm -o test_temperature就能编译运行。整个过程不依赖硬件、不依赖板子上的调试器纯 PC 上跑。实际项目里如果被测函数内部有大量寄存器读写宏例如#define ADC_DATA_REG (*((volatile uint32_t *)0x40012000))这种情况没法靠桩函数解决只能做两件事把寄存器操作提取成可替换的函数或者通过预处理器将其重定向。在测试构建里可以用-DADC_DATA_REGfake_adc_data把宏重定向到一个测试可读写的全局变量。不过长期看更健康的方式是代码里尽量封装抽象的硬件访问层测试替身才容易插入。5.2 Testbed 的实际落地流程比想象中平滑Testbed 门槛高在公司选型、许可证、流程规范上工具本身的使用流程其实很线性。标准流程大致六步导入工程把被测源码目录导入 Testbed它能识别主流嵌入式 IDE 工程比如 IAR、Keil、GCC Makefile。静态分析先做静态检查包括复杂度、规则、数据流。这一步会先暴露不少隐患比如未初始化变量、越界索引提前修掉有意义。插桩与构建Testbed 会自动生成插桩代码在函数入口、出口、分支处做标记用来收集执行状态数据。这个过程完全图形化配置不用手改源码。生成测试驱动代码手动设定正常路径、边界路径、异常路径的输入或者用它的自动用例生成器工具会生成main测试入口和被测函数的调用代码。执行测试与覆盖率统计在 PC 或者目标板上执行Testbed 会收集语句覆盖、分支覆盖、MC/DC 覆盖。这是过认证和出报告时最看重的指标。导出报告能直接导出结构化覆盖率报告和测试报告供审核和归档。对已经跑过流程的团队来说Testbed 最值钱的部分是 MC/DC 覆盖率的自动化。功能安全标准里对条件组合覆盖有硬性要求比如说一个if (a b)你要证明a和b真假组合里每个条件独立决定结果。手工算这些组合极其痛苦Testbed 能在插桩后自动分析并帮助你补充用例这是轻量方案替代不了的。5.3 轻量方案与 Testbed 的边界什么时候别硬上既然轻量方案也能测为什么还要 Testbed我自己的判断标准有三条团队对代码覆盖率有没有硬性合规要求如 ISO 26262、DO-178C有就上 Testbed因为它生成的覆盖率报告和追溯矩阵是被认证机构认可的。被测代码量级有多大上万行纯 C 逻辑时手写 stub 和测试驱动容易漏掉Testbed 的全自动用例管理在量级大时能节省大量人天。项目周期和成本评估Testbed 有较高的授权费用对一个小团队做一两个月的原型验证来说轻量方案更务实。所以结论不是嵌入式必须 Testbed而是合规和规模决定工具逻辑和桩函数决定一切。不管用哪套桩函数和用例设计能力都是通用的。6. vue单元测试报错排查最常见的五个坑的完整定位链路热词里vue单元测试报错这组词说明一个问题Vue 项目配 Vitest 报错的频率真的不低。我团队里新人上手时十个里有七个会卡在配置或者异步问题上。下面把最常出现的五种报错按排查链路完整走一遍。6.1 Error: window is not defined这个报错发生在你测试文件里用了document、window或者组件模板里访问了浏览器 API但测试环境匹配的还是默认的node。排查链路打开vitest.config.ts看test.environment是不是node。改成jsdom重启测试。如果改了还在报可能是某个第三方库在 import 时就访问了window此时要在 setup 文件里给globalThis.window做 shim或者用vi.stubGlobal来模拟。6.2 SyntaxError: Cannot use import statement outside a module这个报错通常出现在组件测试中 import 了一个 CommonJS 格式的第三方库时。排查链路确认该库是不是require()导出的。Vitest 默认会用 Vite 的转换管道处理所有模块但如果测试跑的是.ts直接调ts-node而非 Vite就会出现这个错。确保你用的是vitest命令不是自定义的ts-node脚本。如果第三方包在node_modules里没被 Vite 处理在配置中执行test.server.deps.inline把该包名加进去或者用resolve.alias指向源码而不是编译产物。6.3 Cannot find module components/Button.vueVite 项目里别名默认是配在vite.config.ts里的。测试启动会读vitest.config.ts如果你单独建的 Vitest 配置没有继承 Vite 的resolve.alias它就找不到路径了。排查链路确认vitest.config.ts里是否配置了resolve.alias最好直接读取并复用vite.config里的别名映射。更省事的方式是不单独建vitest.config.ts直接在vite.config.ts里添加test字段并用/// reference typesvitest /声明。Vite 和 Vitest 共用同一个配置文件时别名、插件都天然一致少一半的路径坑。6.4 ResizeObserver is not defined组件里用到了图表库、虚拟滚动库、抽屉组件时太常见了。jsdom 不支持ResizeObserver。排查链路直接在 setup 文件里塞一个空实现这是最通用的做法。如果组件不支持把它 mock 掉考虑在单个测试中vi.stubGlobal(ResizeObserver, class { observe() {} unobserve() {} disconnect() {} })。6.5 异步断言狂报 [ERR_HTTP_INVALID_HEADER_VALUE] 或请求超时这个错误的核心原因往往是测试代码里调用了真实的 HTTP 请求const res await fetch(/api/xxx) // 测试环境实际发出去了排查链路检查被测模块顶部是否 mock 了请求模块。如果没 mock直接新增vi.mock(/api/xxx)。如果 mock 了但仍然发出请求说明你 mock 的模块名和被测模块里 import 的模块名不一致。Vite 模块路径解析对大小写、扩展名很敏感/api/xxx和../api/xxx是两回事。追不到就直接全局 mock fetchvi.stubGlobal(fetch, vi.fn())把网络请求从源头挡掉。排查这类异步问题有个通用技巧在测试失败时console.log一下当前的 call stack 或者把vi.mock放到文件顶部而不是beforeEach里。Vitest 会做模块提升vi.mock会自动提升到文件最上方如果你把它藏在beforeEach里mock 时机不对就会导致依赖没生效行为完全是迷。记住一句话vi.mock永远放在文件顶部不要放在生命周期钩子里需要改变 mock 返回值用mockImplementation不要重复调用vi.mock。7. 最后分享一个提升团队测试意识的小技巧讲了很多工程细节最后说个在团队协作上非常管用的做法每次修复一个 bug 时先写一个会失败的测试再让修复后的代码通过它。一次两次可能觉得多此一举坚持三个月之后你回头看那些最刁钻的回归问题绝大多数都被当初的测试挡在了门外。还有个小点测试代码和人一样也会腐烂。跑一次测试要 10 分钟、断言全是宽松匹配、失败的用例没人愿意修这时候要先停下来清理测试本身而不是继续往上堆用例。把测试当代码一样评审、重构它才能真正成为项目的护城河而不是 CI 里的背景噪音。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

WorkBuddy 光伏方案入门:别只看装机容量,把阴影、负载与回本假设讲明白 2026/10/1 17:02:10

WorkBuddy 光伏方案入门:别只看装机容量,把阴影、负载与回本假设讲明白

WorkBuddy 光伏方案入门:别只看装机容量,把阴影、负载与回本假设讲明白 [!NOTE] 用屋顶面积乘组件功率就得出收益,是常见的过度简化。方案至少要区分资源、朝向、遮挡、系统损耗、负载和电价情景。 本课不会用“AI 一键完成”制造错觉,而是把 WorkBuddy、Python 3.11、pand…

阅读更多 →
UE5地编必会:烘焙光照原理与Lumen差异及实操指南 2026/10/1 17:02:10

UE5地编必会:烘焙光照原理与Lumen差异及实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
前端精读:深入 JavaScript 事件循环(Event Loop)与异步编程原理 2026/10/1 17:02:10

前端精读:深入 JavaScript 事件循环(Event Loop)与异步编程原理

文档技术博客教程 【免费下载链接】weekly 前端精读周刊。帮你理解最前沿、实用的技术。 项目地址: https://gitcode.com/GitHub_Trending/we/weekly 点击查看 免费下载 事件循环(Event Loop)是 JavaScript 运行机制的核心"内科"&…

阅读更多 →
自治数据平台实战:自动化运维与性能调优的架构设计 2026/10/1 17:02:10

自治数据平台实战:自动化运维与性能调优的架构设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
图片像素、分辨率与文件体积怎么算?一文厘清概念与实战 2026/10/1 17:02:09

图片像素、分辨率与文件体积怎么算?一文厘清概念与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
精读《Excel JS API》:从 Range 抽象到 context.sync 的开放 API 设计 2026/10/1 17:02:03

精读《Excel JS API》:从 Range 抽象到 context.sync 的开放 API 设计

文档技术博客教程 【免费下载链接】weekly 前端精读周刊。帮你理解最前沿、实用的技术。 项目地址: https://gitcode.com/GitHub_Trending/we/weekly 点击查看 免费下载 Excel 如今可以利用 JavaScript 根据单元格数据生成图表、表格,或通过 JS 拓展自定…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉