ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

单元测试的优雅与完善:从断言契约到Vue+Pinia实战

单元测试的优雅与完善:从断言契约到Vue+Pinia实战 1. 为什么“优雅且完善”的单元测试不是写完就完事而是工程能力的显性标尺“单元测试”这四个字现在几乎成了任何技术面试开场必问的问题但真正能说清楚“什么叫优雅”、又能在实际项目里写出“完善”测试的人比例远低于你想象。我带过二十多个前后端团队看过上千份PR里的测试代码发现一个扎心的事实80%的所谓“单元测试”本质是把业务逻辑再抄一遍然后用expect(result).toBe(true)收尾——它能跑通但既不优雅也不完善更谈不上工程价值。所谓“优雅”不是指代码多漂亮而是指测试意图清晰、结构可读、维护成本低、与被测代码共生演进。就像你不会用三行嵌套for循环去遍历一个数组也不该用一堆if/else去模拟边界条件所谓“完善”不是覆盖率数字拉到100%而是覆盖了真实业务中会出错的路径、暴露了隐藏的耦合、验证了接口契约、经得起重构考验。一个Math.max(a, b)函数写expect(max(1, 2)).toBe(2)只是及格线而expect(max(-Infinity, 5)).toBe(5)、expect(max(NaN, 3)).toBe(NaN)、expect(max(undefined, 10)).toBe(10)才是完善——它在告诉你这个函数对非数字输入怎么反应是否隐含类型假设边界值是否被忽略你看到的热搜词里“Junit”“断言”“测试覆盖率”是工具和指标“边缘条件”是核心难点“vue router pinia eslint prettier vitest”是前端新栈的落地场景“jmeter beanshell断言”是接口层的误用混淆——它们共同指向一个现实测试不是孤立技能而是贯穿开发全链路的思维习惯。你在Vue组件里mock一个pinia store和在Java服务里用MockBean注入依赖底层逻辑完全一致隔离、可控、可重复。而“断言”从来不是assertThat或expect().toBe()的语法糖它是你对系统行为的明确承诺——这个函数在输入X时必须返回Y且不能有副作用且耗时不能超Z毫秒。没写断言等于没写测试断言写得模糊比如只断言result ! null等于埋下定时炸弹。所以这篇笔记不教你怎么安装Vitest也不罗列JUnit 5所有注解。我要带你拆解的是当你要为一个真实函数、一个Vue组合式API、一个Spring Boot Controller写测试时从第一行describe开始每一步决策背后的工程权衡。你会看到为什么“mock axios”比“mock fetch”更贴近前端真实场景为什么“测试覆盖率85%”可能比“70%”更危险为什么一个beforeEach里初始化10个变量的测试套件注定会在三个月后被所有人绕着走。这不是语法手册这是我在三个不同技术栈、七次大型重构、四次线上故障复盘后亲手划掉又重写的测试实践清单。2. “优雅”的底层逻辑测试结构设计与方案选型的硬核权衡2.1 测试金字塔不是理论模型而是成本分配地图很多人一提单元测试就默认“越底层越好”结果写出一堆紧耦合的私有方法测试重构时改一行业务代码就得动五处测试。真正的“优雅”始于对测试分层本质的理解它不是按代码位置划分controller/service/dao而是按验证目标与执行成本的比值来划分。单元测试Unit Test验证单个函数/方法的纯逻辑输入→输出映射成本最低毫秒级、反馈最快秒级、隔离最强无I/O、无网络、无数据库。它的价值在于让开发者敢改代码。集成测试Integration Test验证模块间协作比如Service调用Repository、Vue组件挂载Router。成本中等百毫秒级、反馈较慢分钟级、需部分环境支撑。它的价值在于暴露胶水代码缺陷。E2E测试End-to-End Test验证完整用户流程比如登录→下单→支付。成本最高秒级、反馈最慢十分钟、环境最重需真实浏览器、后端服务。它的价值在于守住业务主干不崩。提示如果你的单元测试平均执行时间超过50ms大概率在做假单元测试——你可能在启动Spring Context、连接真实数据库、或等待异步Promise。真正的单元测试应该像计算器一样快。我见过最典型的反模式一个Vue组件测试mount()时传入真实Pinia store实例store里又调用了真实API测试脚本里还写了await flushPromises()。这已经不是单元测试是微型E2E执行一次要3秒CI里跑100个这样的测试光测试就占10分钟。优雅的解法是用createPinia()创建空store用jest.mock(axios)拦截HTTP调用用vi.mocked()替换具体函数实现。这样测试只关注组件自身逻辑props传入是否渲染正确、事件触发是否调用预期方法、状态变更是否触发正确副作用。2.2 断言策略从“结果正确”到“行为契约”的跃迁断言Assertion是测试的灵魂但90%的人只停留在“结果对不对”。真正的优雅在于用断言声明系统契约。我们以一个常见场景为例用户注册接口的密码校验。// 被测函数 function validatePassword(password) { if (!password) return { valid: false, message: 密码不能为空 }; if (password.length 8) return { valid: false, message: 密码长度至少8位 }; if (!/[A-Z]/.test(password)) return { valid: false, message: 密码必须包含大写字母 }; return { valid: true, message: }; }初级断言仅验证结果test(密码为空时返回错误, () { const result validatePassword(); expect(result.valid).toBe(false); });问题只断言了valid字段没验证message是否准确。如果未来有人把提示改成“请输入密码”测试照样通过但用户体验已降级。优雅断言验证完整契约test(密码为空时返回精确错误信息, () { const result validatePassword(); expect(result).toEqual({ valid: false, message: 密码不能为空 }); });更进一步用toMatchObject避免过度断言test(密码为空时返回精确错误信息, () { const result validatePassword(); expect(result).toMatchObject({ valid: false, message: 密码不能为空 }); // 不关心是否有其他字段比如timestamp聚焦契约核心 });再看一个易被忽略的维度副作用断言。很多函数不仅返回值还修改外部状态。比如一个购物车添加商品的函数function addToCart(cart, item) { cart.items.push(item); // 修改cart对象 cart.total item.price; return cart; }优雅测试必须验证副作用test(addToCart修改原cart对象并更新total, () { const cart { items: [], total: 0 }; const item { id: 1, price: 99 }; const result addToCart(cart, item); expect(result).toBe(cart); // 验证返回的是原对象引用非深拷贝 expect(cart.items).toHaveLength(1); expect(cart.total).toBe(99); });2.3 工具链选型Vitest vs JUnit不是语言问题而是工程上下文问题热搜词里同时出现“Vitest”和“JUnit”常被误解为“前端用Vitest后端用JUnit”。真相是工具选择取决于你的工程约束而非技术栈归属。维度Vitest前端JUnit 5Java共同底层逻辑执行速度基于Vite利用ESM原生加载冷启动100ms需启动JVM但TestInstance(Lifecycle.PER_CLASS)可复用实例都追求最小化启动开销Mock能力vi.mock()支持动态模块替换vi.fn()可精确控制返回值/调用次数Mockwhen().thenReturn()但需ExtendWith(MockitoExtension.class)核心都是“运行时替换依赖”测试组织describe/it天然支持嵌套适合BDD风格Nested类支持分组但语法稍重都需按业务场景而非代码文件组织CI友好度输出JSON报告可直接接入SonarQubeSurefire插件生成XML兼容所有CI报告格式统一才能聚合分析关键决策点如果你的前端项目已用Vite强行引入Jest会增加构建复杂度需额外配置babel/jest预设而Vitest零配置即可接管。我试过在Vue3Vite项目里同时跑Jest和VitestVitest平均快3倍内存占用低40%。如果你的Java服务使用Spring BootJUnit 5 Mockito是事实标准但要注意MockBean用于Spring Context集成测试而纯单元测试应使用MockInjectMocks避免启动整个Context——后者单测执行时间从200ms飙升到2s。真正的陷阱是“跨层混用”比如用Vitest测Node.js API层本该用Jest或直接用Supertest或用JUnit测Vue组件需额外引入jsdom。这违背了“测试分层”原则导致维护成本指数级上升。3. “完善”的实操要点从边缘条件到测试覆盖率的深度拆解3.1 边缘条件不是“极端情况”而是业务规则的显性化表达热搜词里高频出现“边缘条件”但很多人把它等同于“最大值、最小值、空字符串”。这是巨大误区。边缘条件的本质是业务规则在输入空间中的不连续点。它可能藏在时间、状态、权限、数据精度等任何维度。以电商订单取消为例业务规则可能是“订单创建30分钟内可无理由取消支付成功后不可取消已发货订单需联系客服。”对应的边缘条件绝不仅是cancelTime Date.now() - 30*60*1000而是条件类型具体场景测试要点为什么重要时间边界订单创建时间当前时间-30分钟整cancelAt order.createdAt 30min时是否允许取消时间计算浮点误差、时区转换、数据库时间精度MySQL DATETIME vs TIMESTAMP状态跃迁支付成功瞬间paymentStatus从PENDING→SUCCESS取消操作是否立即拒绝且不产生补偿事务状态机并发安全避免“取消成功但支付到账”的资金漏洞权限边界用户A尝试取消用户B的订单是否返回403而非404防止订单ID枚举攻击安全设计避免信息泄露数据精度订单金额9999999.999超JavaScript Number.MAX_SAFE_INTEGER取消后退款金额是否精确需BigInt或decimal.js金融级精度避免分币误差累积实操技巧用等价类划分边界值分析法系统挖掘列出所有输入参数如orderStatus, paymentStatus, createdAt, userId, currentUserId对每个参数划分有效/无效等价类如orderStatus: [CREATED, PAID, SHIPPED, CANCELLED]在每个等价类边界取值如createdAt:now-30min,now-30min1ms,now-30min-1ms组合高风险组合如orderStatusPAIDpaymentStatusPENDING——支付超时未回调的脏状态我曾在一个支付网关项目里因漏测amount0场景导致优惠券全额抵扣时生成了0元支付单后续对账系统无法识别引发财务对账失败。这个“0”不是技术边缘而是业务规则盲区优惠券规则里写着“满100减20”但没定义“满0减0”是否合法。3.2 测试覆盖率85%的陷阱与100%的幻觉“测试覆盖率”是双刃剑。新手常陷入两个极端要么追求100%覆盖写一堆无意义的getter/setter测试要么觉得“覆盖率70%够用”放任核心算法裸奔。优雅的完善是用覆盖率数据反推质量缺口而非用数字自我麻痹。先明确三个关键概念行覆盖率Line Coverage代码行是否被执行。最容易刷也最没用。if (x 0) { a(); } else { b(); }只测x1覆盖率50%但x-1的分支完全没验证。分支覆盖率Branch Coverage每个if/else、switch case是否都执行。比行覆盖有用但仍有盲区。if (a b)只测atrue,btrue和afalse,bfalse漏掉atrue,bfalse这个关键组合。路径覆盖率Path Coverage所有可能执行路径是否覆盖。理论上最全实践中不可能穷尽n个if嵌套2^n路径。我的实操原则对核心算法强制分支覆盖率100%比如加密解密、排序、数学计算。用c8Vitest内置或JaCoCoJava生成报告重点看红色未覆盖分支。对业务逻辑聚焦“决策点覆盖率”不是每行都要覆盖而是每个if、每个switch、每个try/catch的每个出口都要有对应测试。例如function calculateDiscount(order) { if (order.isVip) { // 决策点1 if (order.total 1000) return 0.2; // 决策点2 return 0.1; } return 0; }需要4个测试isViptrue,total1001、isViptrue,total999、isVipfalse、isViptrue,total1000边界。对胶水代码如DTO转换、日志打印接受低覆盖率这些代码不包含业务逻辑出错影响小过度测试反而拖慢CI。注意Vitest的--coverage默认只统计行覆盖。要启用分支覆盖需在vitest.config.ts中配置export default defineConfig({ test: { coverage: { provider: c8, reporter: [text, html], thresholds: { lines: 80, branches: 80, // 关键强制分支覆盖 functions: 80, statements: 80 } } } });3.3 Vue Pinia Router 的单元测试实战拒绝“挂载即测试”前端框架测试的最大坑是把mount()当成万能钥匙。热搜词里“vue单元测试报错”高频出现90%源于对组件职责的误判。一个Vue组件通常包含三层职责UI渲染逻辑根据props/state渲染DOM交互逻辑响应事件、调用方法、触发副作用集成逻辑调用Router导航、读写Pinia状态、发起API请求优雅测试必须分层验证Step 1纯UI渲染测试无任何依赖!-- UserCard.vue -- template div classuser-card h2{{ user.name }}/h2 p v-ifuser.email{{ user.email }}/p button clickonEdit编辑/button /div /template// UserCard.spec.ts import { shallowMount } from vue/test-utils; import UserCard from /components/UserCard.vue; test(渲染用户姓名和邮箱邮箱存在时, () { const wrapper shallowMount(UserCard, { props: { user: { name: 张三, email: zhangexample.com } } }); expect(wrapper.find(h2).text()).toBe(张三); expect(wrapper.find(p).exists()).toBe(true); expect(wrapper.find(p).text()).toBe(zhangexample.com); }); test(邮箱为空时不渲染p标签, () { const wrapper shallowMount(UserCard, { props: { user: { name: 张三, email: } } }); expect(wrapper.find(p).exists()).toBe(false); // 关键验证v-if逻辑 });用shallowMount而非mount避免渲染子组件如Button聚焦自身逻辑。Step 2交互逻辑测试mock依赖script setup import { useUserStore } from /stores/user; import { useRouter } from vue-router; const props defineProps([user]); const emit defineEmits([edit]); const userStore useUserStore(); const router useRouter(); function onEdit() { userStore.setCurrentUser(props.user); router.push(/user/edit); } /scripttest(点击编辑触发store更新和路由跳转, async () { const mockRouter { push: vi.fn() }; const mockStore { setCurrentUser: vi.fn() }; // 替换全局依赖 vi.mock(vue-router, () ({ useRouter: vi.fn(() mockRouter) })); vi.mock(/stores/user, () ({ useUserStore: vi.fn(() mockStore) })); const wrapper shallowMount(UserCard, { props: { user: { id: 1, name: 张三 } } }); await wrapper.find(button).trigger(click); expect(mockStore.setCurrentUser).toHaveBeenCalledWith({ id: 1, name: 张三 }); expect(mockRouter.push).toHaveBeenCalledWith(/user/edit); });这里的关键是不测试Router或Pinia内部实现只验证组件是否正确调用它们。Router的push是否真跳转由Router自己的单元测试保证。Step 3集成逻辑测试真实环境但隔离I/Otest(编辑后store状态更新且路由生效集成验证, async () { const router createRouter({ ... }); // 创建真实router const pinia createPinia(); // 创建真实pinia const wrapper mount(UserCard, { props: { user: { id: 1, name: 张三 } }, global: { plugins: [router, pinia] } }); // 拦截axios避免真实请求 vi.mock(axios, () ({ default: { get: vi.fn().mockResolvedValue({ data: {} }) } })); await wrapper.find(button).trigger(click); // 验证store状态 const userStore useUserStore(); expect(userStore.currentUser.id).toBe(1); // 验证路由需等待nextTick await nextTick(); expect(router.currentRoute.value.path).toBe(/user/edit); });这种测试成本高只在关键路径如登录、支付上使用非核心组件跳过。4. 实操过程从零开始构建一个可复用的单元测试模板4.1 初始化Vitest TypeScript Vue 3 的最小可行配置不要从网上抄一份“最佳配置”先建一个最小可工作模板再逐步加固。这是我用在所有新项目的起始点Step 1安装核心依赖npm install -D vitest vue/test-utilsnext jsdom # 若用TypeScript确保已安装typescript、types/node npm install -D typescript types/nodeStep 2创建基础配置文件vitest.config.tsimport { defineConfig } from vitest/config; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], test: { // 关键使用jsdom模拟浏览器环境 environment: jsdom, // 自动加载测试文件 include: [src/**/*.{test,spec}.{js,ts}], // 为Vue组件提供全局属性 globals: true, // 启用类型检查需配合tsconfig.json typecheck: { enabled: true, ignoreSourceErrors: false } } });Step 3编写第一个测试src/utils/numberUtils.test.tsimport { roundTo } from /utils/numberUtils; describe(roundTo, () { test(四舍五入到指定小数位, () { expect(roundTo(3.14159, 2)).toBe(3.14); expect(roundTo(2.5, 0)).toBe(3); // 验证银行家舍入 }); test(处理边界值, () { expect(roundTo(0.005, 2)).toBe(0.01); // 0.005四舍五入为0.01 expect(roundTo(-0.005, 2)).toBe(-0.01); // 负数同样处理 }); });为什么这个配置足够优雅environment: jsdom无需启动真实浏览器速度极快DOM API完整。include路径精准避免扫描node_modulesCI中节省30%时间。typecheck.enabled在测试运行时做TS类型检查提前发现roundTo(abc, 2)这类错误。注意不要急着加coverage、mock等高级配置。先让npm run test能在1秒内跑完再迭代。我见过太多团队卡在“配置不完备就不写测试”的死循环里。4.2 断言库选型expect() vs should() vs assert()哪个更贴近人类思维Vitest默认用expect()但很多人纠结“should”风格user.should.be.defined或传统assertassert.equal(a, b)。我的经验是断言风格应服务于可读性而非个人偏好。对比三种写法对同一场景的表达// 场景验证API返回的用户列表不为空且包含正确字段 const response await api.getUsers(); // expect风格Vitest默认 expect(response.data).toHaveLength(5); expect(response.data[0]).toMatchObject({ id: expect.any(Number), name: expect.any(String), email: expect.stringMatching(//) }); // should风格需额外安装chai response.data.should.have.lengthOf(5); response.data[0].should.include.keys(id, name, email); // assert风格Node.js内置 assert.strictEqual(response.data.length, 5); assert.ok(typeof response.data[0].id number);为什么expect是更优解错误信息更友好expect(x).toBe(y)失败时显示Expected: 5, Received: 4assert.equal(x,y)只显示AssertionError需手动加message。链式调用更自然expect(fn()).rejects.toThrow(Network Error)一行搞定异步错误断言should需should.throw()嵌套。类型推断更准TypeScript能自动推导expect(...).toBe(...)的参数类型减少as any滥用。实操技巧善用Vitest的自定义匹配器让断言更贴近业务语言// src/test-utils/matchers.ts declare module vitest { interface AssertionT { toBeValidEmail(): void; } } expect.extend({ toBeValidEmail(received: string) { const pass typeof received string /^[^\s][^\s]\.[^\s]$/.test(received); if (pass) { return { message: () expected ${received} not to be a valid email, pass: true }; } else { return { message: () expected ${received} to be a valid email, pass: false }; } } }); // 在测试中使用 test(邮箱格式校验, () { expect(testexample.com).toBeValidEmail(); expect(invalid-email).not.toBeValidEmail(); });4.3 Mock策略何时该mock何时该真实调用Mock不是为了“让测试通过”而是为了控制变量聚焦验证目标。新手常犯两大错误mock过度连Math.random都mock或mock不足让测试依赖网络。黄金法则只mock你无法控制的外部依赖。✅ 应mockAPI调用axios/fetch、第三方SDK微信JS-SDK、全局对象window.location、随机数生成器Math.random❌ 不应mock语言内置方法Array.prototype.map、项目内纯函数utils/numberUtils.ts、可预测的同步逻辑Vitest Mock实操指南模块级Mock推荐在测试文件顶部vi.mock(axios)影响整个文件。函数级Mock精准const mockFn vi.fn()用于验证调用次数/参数。动态Mock灵活vi.mock(./api, () ({ getUser: vi.fn().mockResolvedValue(...) }))// 测试API调用 import { getUser } from /api/user; vi.mock(/api/user, () ({ getUser: vi.fn() })); test(加载用户时显示loading状态, async () { const mockGetUser vi.mocked(getUser); mockGetUser.mockResolvedValue({ id: 1, name: 张三 }); const wrapper mount(UserProfile, { props: { userId: 1 } }); await wrapper.vm.$nextTick(); expect(wrapper.find(.loading).exists()).toBe(true); await vi.waitFor(() { expect(wrapper.find(.user-name).text()).toBe(张三); }); });关键技巧用vi.mocked()包裹被mock函数获得TypeScript类型提示避免mockReturnValue拼写错误。4.4 测试生命周期管理beforeEach不是万能初始化器beforeEach常被滥用为“把所有东西都初始化一遍”的垃圾桶。结果是一个测试套件里beforeEach创建了10个对象、调用了3个API、设置了5个state而某个测试只用到了其中2个。优雅的生命周期管理原则beforeEach只做绝对必需的、所有测试共用的初始化如创建wrapper、设置全局mock。每个测试内按需创建专属对象避免状态污染。用afterEach清理副作用如清除localStorage、重置计时器。// 反模式过度初始化 beforeEach(() { vi.mock(axios); vi.mock(/stores/user); vi.mock(/composables/useAuth); // ... 还有7个mock wrapper mount(Component, { props: {...} }); }); // 正确模式按需初始化 test(用户未登录时显示登录按钮, () { // 只mock需要的依赖 vi.mock(/composables/useAuth, () ({ useAuth: vi.fn(() ({ isAuthenticated: false })) })); const wrapper mount(Component); expect(wrapper.find(button.login).exists()).toBe(true); }); test(用户已登录时显示用户名, () { vi.mock(/composables/useAuth, () ({ useAuth: vi.fn(() ({ isAuthenticated: true, user: { name: 张三 } })) })); const wrapper mount(Component); expect(wrapper.find(.user-name).text()).toBe(张三); });5. 常见问题与排查技巧实录那些没人告诉你的“踩坑现场”5.1 “Vue组件测试报错Cannot find module ‘vue’” —— 不是缺包是TS路径别名没配这个错误90%不是真的没装vue而是Vitest的TS解析器找不到/components这类别名路径。解决方案分三步确认tsconfig.json中baseUrl和paths配置正确{ compilerOptions: { baseUrl: ., paths: { /*: [src/*], ~~/*: [src/*] } } }在vitest.config.ts中显式配置resolve.aliasVitest不自动读取tsconfigimport { defineConfig } from vitest/config; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], resolve: { alias: { : /src, ~~: /src } }, test: { // ... } });重启Vitest服务Vitest不会热重载tsconfig必须CtrlC再npm run test。实测心得如果用pnpm还需在pnpm-workspace.yaml中确保vue/runtime-core等peer依赖版本一致否则会出现“Found multiple Vue versions”警告。5.2 “测试覆盖率显示100%但线上还是出Bug” —— 覆盖率数字的欺骗性我经历过最痛的一次支付回调测试覆盖率98%上线后因req.body是Buffer而非String导致签名验签失败。覆盖率工具只检测代码行是否执行不检测数据类型是否符合预期。根本原因覆盖率工具无法识别“类型断言缺失”。const data req.body; sign(data)中data本该是object但实际是Buffer而测试用{ body: {} }mock类型正确但数据形态错误。覆盖率无法捕获“环境差异”。测试在Linux下用process.env.NODE_ENVtest线上是production而某段代码if (process.env.NODE_ENV development)被跳过但该分支有逻辑错误。破解方案在测试中强制类型校验test(支付回调body必须是object, () { const req { body: Buffer.from({id:1}) } as any; // 模拟真实Buffer expect(() handleCallback(req)).toThrow(Invalid request body); });用cross-env在CI中模拟多环境# package.json scripts test:prod: cross-env NODE_ENVproduction vitest5.3 “Jest/Vitest测试通过但CI里失败” —— 本地与CI的环境鸿沟典型现象本地npm run test全绿CI里一堆ReferenceError: window is not defined。这不是bug是环境配置差异。根因分析表差异点本地环境CI环境解决方案Node.js版本v18.17.0v16.20.0旧版CI镜像在.nvmrc或package.json中声明engines: {node: 18.0.0}CI中用nvm install全局对象Chrome DevTools提供windowCI用jsdom但未配置globalThis.window在vitest.config.ts中加setupFiles: [./src/test-setup.ts]内容为globalThis.window {} as any时区本地时区CSTCI服务器时区UTC测试中用vi.setSystemTime(new Date(2023-01-01))固定时间避免new Date().getHours()波动CI专用配置模板.github/workflows/test.ymlname: Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-nodev3 with: node-version: 18 - run: npm ci - run: npm run test:ci env: TZ: Asia/Shanghai # 强制时区避免时间相关测试失败5.4 “Mock函数不生效总是调用真实实现” —— Mock时机与作用域陷阱最隐蔽的坑vi.mock(axios)写了但测试里还是发真实请求。原因通常是mock语句位置错误。正确写法必须在import之前// ✅ 正确mock在import前且是顶层语句 vi.mock(axios); import { api } from /api; test(调用api时发送正确URL, async () { axios.get.mockResolvedValue({ data: [] }); await api.getData(); expect(axios.get).toHaveBeenCalledWith(/api/data); });错误写法// ❌ 错误1mock在import之后 import { api } from /api; vi.mock(axios); // 此时axios已被importmock无效 // ❌ 错误2mock在test内部 test(..., () { vi.mock(axios); // 作用域仅限于此test且执行太晚 // ... });终极调试技巧在测试中加一行console.log(axios.get)如果输出[Function: mocked
返回列表