STM32G431 ADC开发实战:从基础配置到高精度多通道采集
2026/8/28 2:15:08
单元测试是开发者针对「最小功能单元」(工具函数、单个组件、状态逻辑等)编写的自动化测试脚本,通过工具执行验证逻辑正确性,并非额外负担,而是提前规避风险、降低长期成本的开发必备环节,核心价值体现在 3 个维度:
在 Vue3 项目中,Vitest 是官方推荐的单元测试工具,相比 Jest、Mocha 等工具,适配性与效率优势显著,核心原因的 5 点:
@vue/test-utils/@testing-library/vue可快速实现组件测试;test/expect/vi语法与 Jest 完全一致,原 Jest 项目可直接迁移,无需重新学习 API;Vitest 本质是「Vite 生态+测试核心模块」的整合工具,核心原理拆解为 4 大模块,流程清晰易懂:
src/**/*.test.ts)扫描文件,识别describe/test用例与生命周期钩子,构建用例树;jsdom/happy-dom模拟浏览器环境(支持document/window),也可直接用 Node 环境,适配不同测试场景;expect断言、vi对象 Mock 能力,集成istanbul统计代码覆盖率,输出多格式报告。Vitest 用法聚焦「工具函数、Vue 组件、Pinia 状态、接口 Mock」4 大核心场景,语法简洁,可直接套用:
describe('模块名', () => { 用例集合 });test('用例描述', () => { 断言逻辑 });expect(实际结果).匹配器(预期结果)(如toBe/toEqual/toBeInTheDocument);vi.fn()(函数 Mock)、vi.mock('模块')(模块 Mock)、vi.useFakeTimers()(定时器 Mock);beforeAll(全局前置)、beforeEach(每个用例前置,重置状态用)、afterEach/afterAll(后置)。src/utils/format.ts// 金额格式化:保留2位小数+千分位exportconstformatMoney=(num:number):string=>{if(isNaN(num))return'0.00'returnnum.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,',')}src/utils/format.test.tsimport{test,expect}from'vitest'import{formatMoney}from'./format'describe('formatMoney 工具函数',()=>{test('正常正数:输入1234,返回"1,234.00"',()=>{expect(formatMoney(1234)).toBe('1,234.00')})test('负数:输入-567.8,返回"-567.80"',()=>{expect(formatMoney(-567.8)).toBe('-567.80')})test('异常值:输入NaN,返回"0.00"',()=>{expect(formatMoney(NaN)).toBe('0.00')})})src/components/MyButton.vue<template> <button class="btn" :disabled="disabled" @click="handleClick"> {{ label }}({{ count }}次点击) </button> </template> <script setup lang="ts"> import { ref } from 'vue' defineProps<{ label: string; disabled?: boolean }>() const emit = defineEmits<{ (e: 'click'): void }>() const count = ref(0) const handleClick = () => { if (!props.disabled) { count.value++ emit('click') } } </script>src/components/MyButton.test.tsimport{test,expect}from'vitest'import{render,screen,fireEvent}from'@testing-library/vue'importMyButtonfrom'./MyButton.vue'describe('MyButton 组件',()=>{// 渲染测试test('渲染正确的 label 文本',()=>{render(MyButton,{props:{label:'提交'}})expect(screen.getByText('提交(0次点击)')).toBeInTheDocument()})// 交互测试:正常点击test('点击按钮触发事件,count 自增',async()=>{constmockClick=vi.fn()render(MyButton,{props:{label:'点击'},attrs:{onClick:mockClick}})constbtn=screen.getByText('点击(0次点击)')awaitfireEvent.click(btn)// 模拟点击(async/await 处理 DOM 异步)expect(mockClick).toHaveBeenCalledTimes(1)// 事件触发expect(screen.getByText('点击(1次点击)')).toBeInTheDocument()// count 自增})// 边界测试:禁用状态test('禁用状态下,点击不触发事件',async()=>{constmockClick=vi.fn()render(MyButton,{props:{label:'禁用',disabled:true},attrs:{onClick:mockClick}})awaitfireEvent.click(screen.getByText('禁用(0次点击)'))expect(mockClick).not.toHaveBeenCalled()// 事件未触发expect(screen.getByText('禁用(0次点击)')).toBeDisabled()// 按钮禁用})})src/stores/user.tsimport{defineStore}from'pinia'exportconstuseUserStore=defineStore('user',{state:()=>({name:'',age:0,isLogin:false}),actions:{login(userInfo:{name:string;age:number}){this.name=userInfo.namethis.age=userInfo.agethis.isLogin=true},logout(){this.$reset()// 重置状态}}})src/stores/user.test.tsimport{test,expect,beforeEach}from'vitest'import{createPinia,setActivePinia}from'pinia'import{useUserStore}from'./user'// 每个用例前重置 Pinia,避免状态污染beforeEach(()=>{setActivePinia(createPinia())})describe('user Pinia 状态',()=>{test('初始状态正确',()=>{conststore=useUserStore()expect(store.name).toBe('')expect(store.isLogin).toBe(false)})test('login 方法:登录后状态更新',()=>{conststore=useUserStore()store.login({name:'张三',age:25})expect(store.name).toBe('张三')expect(store.isLogin).toBe(true)})test('logout 方法:登出后状态重置',()=>{conststore=useUserStore()store.login({name:'张三',age:25})store.logout()expect(store.name).toBe('')expect(store.isLogin).toBe(false)})})src/api/user.tsimportaxiosfrom'axios'exportconstgetUserInfo=async(id:number)=>{constres=awaitaxios.get(`/api/user/${id}`)returnres.data}src/api/user.test.tsimport{test,expect,vi}from'vitest'importaxiosfrom'axios'import{getUserInfo}from'./user'// Mock 整个 axios 模块,避免真实请求vi.mock('axios')test('getUserInfo:请求成功返回用户数据',async()=>{// 自定义 Mock 接口返回值constmockData={id:1,name:'张三',age:25}(axios.getasReturnType<typeofvi.fn>).mockResolvedValue({data:mockData})constresult=awaitgetUserInfo(1)expect(result).toEqual(mockData)// 返回值正确expect(axios.get).toHaveBeenCalledWith('/api/user/1')// 请求参数正确})test('getUserInfo:请求失败返回默认值',async()=>{// Mock 接口失败(axios.getasReturnType<typeofvi.fn>).mockRejectedValue(newError('请求失败'))constresult=awaitgetUserInfo(999).catch(()=>({id:0,name:'未知'}))expect(result).toEqual({id:0,name:'未知'})// 异常处理正确})src/utils/timer.tsexportconstdelayAlert=(msg:string,delay:number)=>{returnnewPromise((resolve)=>{setTimeout(()=>{console.log(msg)resolve(msg)},delay)})}src/utils/timer.test.tsimport{test,expect,vi}from'vitest'import{delayAlert}from'./timer'test('delayAlert:延迟后返回正确信息',async()=>{vi.useFakeTimers()// 启用假定时器,替代真实时间constmockLog=vi.spyOn(console,'log').mockImplementation()// Mock console.log// 调用函数(不等待真实延迟)constpromise=delayAlert('测试延迟',1000)expect(mockLog).not.toHaveBeenCalled()// 定时器未触发vi.runAllTimers()// 手动触发所有定时器,立即执行constresult=awaitpromise// 等待 Promise 完成expect(result).toBe('测试延迟')// 返回值正确expect(mockLog).toHaveBeenCalledWith('测试延迟')// log 执行// 还原真实定时器和 log,避免污染vi.useRealTimers()mockLog.mockRestore()})# 核心依赖:Vitest + 浏览器环境 + Vue 测试库(二选一)npminstallvitest jsdom -D# 选1:@testing-library/vue(侧重用户行为,推荐)npminstall@testing-library/vue @testing-library/jest-dom -D# 选2:@vue/test-utils(Vue 官方,API 简洁)npminstall@vue/test-utils -D# 可选:可视化 UI + 覆盖率依赖(已内置,按需安装)npminstall@vitest/ui -Dvitest.config.ts(根目录,测试核心配置)import{defineConfig}from'vitest/config'importVuefrom'@vitejs/plugin-vue'// 复用 Vite Vue 插件importpathfrom'path'exportdefaultdefineConfig({plugins:[Vue()],// 解析 Vue 单文件组件test:{environment:'jsdom',// 模拟浏览器环境(必配,否则无 DOM API)include:['src/**/*.{test,spec}.{js,ts,jsx,tsx}'],// 测试文件匹配规则exclude:['src/main.ts','src/App.vue'],// 排除入口文件alias:{'@':path.resolve(__dirname,'./src')},// 路径别名(和 Vite 一致)setupFiles:['src/test/setup.ts'],// 测试前置配置(可选)coverage:{// 覆盖率配置(可选)include:['src/**/*.{vue,ts}'],reporter:['text','html'],// 文本+HTML 报告(打开 coverage/index.html 查看)},},})src/test/setup.ts(测试前置初始化,可选但推荐)import{cleanup}from'@testing-library/vue'import'@testing-library/jest-dom/vitest'// 扩展 DOM 断言(如 toBeInTheDocument)// 每个用例结束后清理 DOM,避免污染afterEach(()=>{cleanup()})// 可选:全局挂载公共组件/指令(如 Button、自定义指令)// import { mount } from '@testing-library/vue'// import MyButton from '@/components/MyButton.vue'// vi.mock('@/components/MyButton.vue', () => ({ default: MyButton }))package.json测试脚本{"scripts":{"test":"vitest run",// 一次性执行所有测试(CI/上线前用)"test:watch":"vitest",// 监听文件,实时重跑(开发时用,推荐)"test:ui":"vitest --ui",// 启动可视化 UI(调试用,http://localhost:51204)"test:cov":"vitest run --coverage"// 执行测试+生成覆盖率报告}}format.test.ts);npm run test:watch,若终端显示「✅ 用例通过」,则集成成功;vitest-plugin-auto-expect(自动生成基础断言,失败时一键补全)。vitest-describe生成分组、vitest-test生成用例、vtu-render生成组件渲染代码;y,自动生成expect断言,无需手动写预期结果。npm run test:watch:仅重跑修改文件关联的用例,文件保存秒级反馈;p:输入文件名,精准重跑单个文件用例;t:输入关键词,重跑匹配用例组/用例;f:仅重跑失败用例(调试 bug 聚焦核心);npm run test:ui,浏览器勾选单个用例重跑,直观查看失败原因+DOM 快照。vi对象 Mock,不用手动造数据(如vi.fn()模拟函数、vi.mock('axios')模拟接口);npm run test:cov生成 HTML 报告,打开coverage/index.html;describe('模块名', () => { test('场景+预期结果', () => {}) })(如test('禁用按钮+点击不触发事件', () => {}));format.test.ts);tsconfig.json:compilerOptions.types添加vitest/globals,避免test/expect类型报错;beforeEach重置状态(Pinia/组件/DOM),避免用例污染;async/await:DOM 交互、接口请求、定时器测试,必须用async/await包裹,避免断言提前执行。单元测试是 Vue3 项目「质量保障+效率提升」的核心手段,解决了 bug 滞后、回归低效、代码混乱、协作成本高等核心痛点;Vitest 凭借「极速体验、Vue3 原生适配、低学习成本」成为首选工具,通过「工具函数+组件+Pinia+Mock」四大场景覆盖核心测试需求,配合提效方案可大幅降低测试成本,实现「短期小投入,长期大收益」。
建议从核心代码入手逐步落地,优先覆盖高频场景,再逐步完善覆盖率,让单元测试融入开发流程,而非额外负担,最终实现项目稳定迭代、团队高效协作。