1. 为什么第57天必须补上测试这堂课
1.1 阶段定位:测试为什么放在“进阶与生态整合”里
学Vue到第57天,大多数人的状态是:能熟练写组件、路由、状态管理,能独立完成一个前后端分离的中后台页面,甚至已经在公司项目里接手了需求。但仔细想想,项目一改动就担心“这里是不是改坏了”,新功能上线前只能靠手点一遍页面,回归测试全靠肉眼和运气——这就是没系统接触测试的典型症状。
“Vue 测试入门”被放在第四阶段的“进阶与生态整合”里,逻辑其实很清晰:入门阶段语法都不会,写了测试大概率是在给代码添堵,遇到报错分不清是功能问题还是测试代码问题;到了第57天,组件化思维已经建立,也经历过项目迭代,这时候引入测试才能真正理解“测试是为了保护已有功能”,而不是“为了完成指标而写的额外代码”。这个阶段补测试,恰恰是性价比最高的时候。
单元测试关注的是“最小代码单元”的行为是否符合预期。在Vue里大部分情况下这个最小单元就是组件、组合式函数(composables)、工具函数以及Pinia的store。端到端测试(E2E)则是站在用户视角,从浏览器地址栏输入开始,验证登录、跳转、数据渲染、交互反馈整条链路是否通畅。两者一个偏“白盒”,一个偏“黑盒”,合在一起才构成对应用的完整保护网。
1.2 单元测试和端到端测试的分工,别搞反了
很多第一次接触测试的人容易陷入一个误区:既然要写测试,那就把页面上的每个按钮点击都写成E2E测试。这是典型的“用大炮打蚊子”,一条E2E用例跑完可能需要十几秒甚至更久,几十个页面叠加起来,测试执行时间直接拖垮开发节奏。
更合理的分工是这样的:
- 组件内部的逻辑判断、计算属性、事件触发、props更新、条件渲染——用单元测试覆盖,毫秒级执行,跑一次几百条用例也就几秒钟;
- 跨页面跳转、权限拦截、表单提交后的数据刷新、路由守卫逻辑——用E2E覆盖,数量控制在十几到几十条,只验证关键业务流;
- 纯函数类工具(比如日期格式化、金额转换、权限校验函数)——单元测试覆盖性价比极高,一个函数几条断言就搞定,后续改动立刻能发现是否影响其他地方。
一句话总结:单元测试管“零件”,E2E管“整机”。零件坏了整机必然出问题,但整机没问题不代表每个零件都合格。两条腿都走稳,才有资格说“这项目测试覆盖到位了”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 单元测试:从安装到第一个能跑的用例
2.1 测试框架选型:为什么是 Vitest + Vue Test Utils
Vue生态里的单元测试方案目前主流是Vitest + @vue/test-utils,也有不少老项目还在用Jest。我的建议是:新项目直接上Vitest,老项目如果是Jest且跑得好好的,不急着迁移,但如果是新建的测试模块,依然建议Vitest。
Vitest和Vite同源,天然复用Vite的配置和依赖解析,启动速度比Jest快一个量级。它原生支持ESM(ECMAScript模块),这让Vue单文件组件的测试过程省去了一大堆转译配置。Jest在Vue项目里通常需要单独配moduleNameMapper处理别名、配transform处理.vue文件和TS,稍微有个版本升级就可能导致整个测试环境崩掉。Vitest开箱即用,配一次以后基本不用管。
@vue/test-utils是Vue官方提供的组件测试工具库,提供了mount、shallowMount、trigger、setProps、find等一系列操作组件实例的API。配合Vitest的断言和mock能力,可以覆盖绝大多数组件测试场景。至于@vue/test-utils和Vitest的版本兼容性,建议都装最新稳定版,官方文档里的安装命令直接复制即可,比手动记版本号靠谱得多。
2.2 初始化测试环境配置
以一个标准的Vite + Vue 3 + TypeScript项目为例,安装依赖只需要三条命令:
bash复制npm install -D vitest @vue/test-utils @vitest/ui jsdom
这里有几个点值得展开说一下。
第一,jsdom是必须装的。Vitest默认运行在Node环境,没有DOM相关能力,而Vue组件测试需要操作元素、派发事件、检查渲染结果,所以必须模拟一个浏览器DOM环境。jsdom是目前最常用的轻量级实现。如果你用的Vite版本较新,也可以考虑happy-dom,启动速度更快,但部分底层API实现不如jsdom稳定,遇到诡异报错时切回jsdom通常能解决。
第二,@vitest/ui是一个可视化面板。装了它之后可以执行vitest --ui,在浏览器里实时查看用例执行情况、每个断言通过与否、测试耗时统计,调试体验比纯命令行好很多,尤其是E2E和单元测试并存时,可以用它方便地切换查看。
接下来在vite.config.ts里加测试配置:
typescript复制/// <reference types="vitest" />
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'istanbul',
include: ['src/components/**/*.vue', 'src/stores/**/*.ts'],
exclude: ['src/main.ts', 'src/router/**/*.ts']
}
}
})
globals设为true后,可以在用例里直接用describe、it、expect等全局函数,不需要在每个文件顶部手动import。setupFiles指定的文件会在每个测试文件执行前运行,通常用来做全局mock或注册公共插件。
还需要在tsconfig.json里加两行,否则TypeScript会找不到vitest的全局类型:
json复制{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
最后在package.json里加命令:
json复制{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}
vitest是watch模式,开发时改一行代码立刻跑相关用例;vitest run是单次执行,适合CI流程。
2.3 写第一个组件测试:渲染、props、事件
先看一个最简单的场景。假设有一个Counter组件:
vue复制<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
initialCount?: number
}>()
const count = ref(props.initialCount ?? 0)
function increment() {
count.value++
}
</script>
<template>
<div>
<p class="count">{{ count }}</p>
<button class="increment" @click="increment">增加</button>
</div>
</template>
对应的测试文件长这样:
typescript复制import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter', () => {
it('默认渲染初始值0', () => {
const wrapper = mount(Counter)
expect(wrapper.find('.count').text()).toBe('0')
})
it('传入initialCount后渲染对应数值', () => {
const wrapper = mount(Counter, {
props: {
initialCount: 10
}
})
expect(wrapper.find('.count').text()).toBe('10')
})
it('点击增加按钮后count加1', async () => {
const wrapper = mount(Counter, {
props: {
initialCount: 1
}
})
await wrapper.find('.increment').trigger('click')
expect(wrapper.find('.count').text()).toBe('2')
})
})
这里有三个容易踩的坑。
第一个,获取元素内容用的是.text(),不是textContent或innerHTML。@vue/test-utils在wrapper上封装了text、html、classes、attributes等方法,直接调用即可。这两个API里,text()返回去掉html标签后的文本内容,html()返回完整HTML字符串,断言时用哪个取决于你关心什么。
第二个,触发交互事件之后必须await。Vue的DOM更新是异步的,trigger返回的是一个Promise,不await的话断言可能跑在重新渲染之前,拿到的是旧值。这是新手写测试报“明明点击了但断言失败”的最常见原因。
第三个,判断存在性时用find返回的wrapper是不是truthy来判断是错的。find找不到元素时返回的是一个“不存在的wrapper”,不会抛错,但它本身是truthy的。正确写法是expect(wrapper.find('.count').exists()).toBe(true),或基于findAll().length来断言。
2.4 异步问题:nextTick、定时器和请求mock
真实项目里组件不会这么简单,至少会涉及异步数据请求、条件渲染、弹窗交互。这些场景下的测试写法要更有针对性。
带v-if的异步渲染,用nextTick控制时序:
typescript复制import { nextTick } from 'vue'
it('条件渲染在数据更新后变化', async () => {
const wrapper = mount(SomeComponent)
expect(wrapper.find('.loading').exists()).toBe(true)
// 触发数据变更
await wrapper.vm.loadData()
await nextTick()
expect(wrapper.find('.loading').exists()).toBe(false)
expect(wrapper.find('.content').exists()).toBe(true)
})
组件内部有setTimeout或setInterval时,Vitest支持伪造定时器:
typescript复制import { vi } from 'vitest'
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('防抖搜索延时生效', async () => {
const wrapper = mount(SearchInput)
await wrapper.find('input').setValue('vue')
// 推进2秒,触发防抖回调
vi.advanceTimersByTime(2000)
await nextTick()
expect(wrapper.emitted('search')).toBeTruthy()
})
emitted是@vue/test-utils提供的API,可以用它验证组件是否向父组件派发了指定事件,非常适合测试“某个交互发生后有没有触发emit”。
对于axios或fetch请求,通常用vi.mock在测试文件里统一mock掉请求模块,然后通过可控的返回值来验证组件在不同数据状态下的表现:
typescript复制import { vi, beforeEach } from 'vitest'
vi.mock('@/api/user')
import { getUserInfo } from '@/api/user'
beforeEach(() => {
vi.mocked(getUserInfo).mockResolvedValue({
name: '张三',
role: 'admin'
})
})
这里有个实操心得:不要试图在测试里真实发起网络请求。测试环境里的请求要么被jsdom拦截返回404,要么会因为网络波动导致测试时好时坏。写单元测试时把所有外网依赖都mock掉,这是铁律。
3. 进阶:路由、Pinia和业务函数的测试方案
3.1 带vue-router的组件怎么测
组件内部用了useRoute或useRouter时,直接mount会报错,需要在测试文件里注入路由。Vue Router官方提供了一个专门用于测试的内存路由实例方案,常见做法是在setupFiles里创建router实例:
typescript复制// src/test/setup.ts
import { createRouter, createMemoryHistory } from 'vue-router'
import { routes } from '@/router'
const router = createRouter({
history: createMemoryHistory(),
routes
})
router.push('/some-path')
await router.isReady()
export { router }
然后在mount组件时通过global选项注入:
typescript复制import { router } from '@/test/setup'
it('从URL读取路由参数', () => {
router.push('/user/123')
const wrapper = mount(UserDetail, {
global: {
plugins: [router]
}
})
expect(wrapper.find('.user-id').text()).toBe('123')
})
这里有个容易忽略的细节:router.push之后一定要await router.isReady()或await router.push()返回的Promise,确保路由切换完成后组件才能拿到最新的路由状态。
如果只是简单模拟路由信息而不需要真正的路由实例,更轻量级的做法是用global.mocks:
typescript复制const $route = {
params: { id: '123' },
query: { keyword: 'vue' }
}
mount(UserDetail, {
global: {
mocks: {
$route
}
}
})
这种方式适合只需要读取路由参数的场景,不需要处理完整路由功能,代码更简洁。但缺点是useRoute调用时mock不生效,因为useRoute走的是inject机制,不走全局属性。我的习惯是:组件里用了useRoute就用真实的内存路由,用了Options API的this.$route就用mocks。
3.2 带Pinia的组件怎么测
Pinia的测试相对简单,因为Pinia设计时就把测试纳入了一等公民。关键点在于每个测试文件里要创建全新的Pinia实例,避免store状态跨用例污染:
typescript复制import { createPinia, setActivePinia } from 'pinia'
import { beforeEach } from 'vitest'
import { useUserStore } from '@/stores/user'
beforeEach(() => {
setActivePinia(createPinia())
})
it('修改store状态后组件正确渲染', async () => {
const store = useUserStore()
store.setUserName('李四')
const wrapper = mount(UserGreeting)
expect(wrapper.find('.name').text()).toBe('李四')
})
store内部调用了外部API怎么办?和前面一样,mock掉对应的请求函数即可。因为Pinia store在测试里就是一个普通对象,你可以在测试中直接调用store里的action,也可以先设置state再断言视图。
测试store本身时,不需要mount任何组件,直接操作store实例就行:
typescript复制it('store的addToCart逻辑正确', async () => {
const cart = useCartStore()
await cart.addItem({ id: 1, name: '测试商品' })
expect(cart.items).toHaveLength(1)
expect(cart.totalPrice).toBe(cart.items[0].price)
})
这种“无DOM”的store测试非常轻量,跑起来几乎不耗时,适合做业务核心逻辑的回归守护。对于依赖其他store的store,需要先创建所有依赖的store的初始状态,然后手动注入,或者把依赖store的action整体mock掉。
3.3 覆盖业务核心逻辑:工具函数与组合式函数测试
除了组件,项目里最容易出bug的地方其实是工具函数和组合式函数。这类代码不依赖DOM,测试写起来最顺手,但往往被忽略。
工具函数示例:
typescript复制// utils/format.ts
export function formatPrice(price: number): string {
return `¥${price.toFixed(2)}`
}
// format.spec.ts
import { formatPrice } from './format'
describe('formatPrice', () => {
it('正常数值格式化为人民币字符串', () => {
expect(formatPrice(10)).toBe('¥10.00')
})
it('小数四舍五入', () => {
expect(formatPrice(10.345)).toBe('¥10.35')
})
it('负数同样支持', () => {
expect(formatPrice(-3.1)).toBe('¥-3.10')
})
it('NaN返回默认值', () => {
expect(formatPrice(NaN)).toBe('¥0.00')
})
})
组合式函数测试更接近组件测试但不用挂载DOM:
typescript复制import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('初始值为0,increment后变1', () => {
const { count, increment } = useCounter()
expect(count.value).toBe(0)
increment()
expect(count.value).toBe(1)
})
it('支持传入初始值', () => {
const { count } = useCounter(10)
expect(count.value).toBe(10)
})
})
这类测试的价值在于:当工具函数被很多地方引用时,改动它的逻辑会导致哪些调用方出错,不需要靠人工去猜,跑一遍测试就能定位。我在实际项目中甚至见过只给utils目录写了测试、组件测试覆盖很低的项目,这类项目虽然测试金字塔不够匀称,但核心业务逻辑出问题的概率已经比不写测试时低很多了。
4. 端到端测试:用Cypress把关键用户流跑通
4.1 E2E测试的定位与选型
单元测试能解决“函数逻辑对不对”的问题,但解决不了“用户能不能完成一次完整的操作”的问题。登录、跳转、提交表单、页面刷新后数据保持——这些跨组件协作的流程,必须用端到端测试兜底。
目前Vue社区里E2E测试工具主要两个选择:Cypress和Playwright。Cypress进入市场早,生态成熟,中文文档和社区资料丰富,Vue官方文档里也有专门的集成说明;Playwright是后来者,性能和跨浏览器支持有优势,但在Vue项目里的集成度和社区实践不如Cypress完整。
我的经验是:Vue项目入门E2E首推Cypress。原因很朴素——当你遇到问题时,用Cypress大概率搜得到一个现成解决方案,用Playwright可能还得自己翻英文issue。等E2E测试玩熟练了,再评估是否要切换到Playwright也不迟。
4.2 Cypress安装与配置
在项目根目录执行:
bash复制npm install -D cypress
npx cypress open
首次打开会让选择测试类型,选择E2E Testing后会自动创建cypress目录和配置文件。
cypress.config.js配置几个关键项:
javascript复制const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:5173',
supportFile: 'cypress/support/e2e.js',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 10000,
}
})
baseUrl是指你开发服务器的地址,通常是Vite默认的5173端口。viewportWidth和viewportHeight决定测试窗口大小,建议保持桌面端标准尺寸,真实还原用户浏览器环境。
启动E2E测试前,必须先启动开发服务器。Cypress不会替你启动项目,所以一般会配合一个小插件“start-server-and-test”,一条命令同时搞定:
bash复制npm install -D start-server-and-test
然后改package.json:
json复制{
"scripts": {
"test:e2e": "start-server-and-test 'npm run dev' http://localhost:5173 'cypress open'"
}
}
这样执行npm run test:e2e时会自动启服务、等服务就绪、再打开Cypress调试面板。
4.3 编写第一个端到端用例:登录到首页跳转
以一个登录页到后台首页的完整流程为例:
javascript复制describe('登录到首页跳转流程', () => {
beforeEach(() => {
cy.visit('/login')
})
it('输入正确账号密码后进入首页', () => {
cy.get('[data-test="username"]').type('admin')
cy.get('[data-test="password"]').type('123456')
cy.get('[data-test="login-btn"]').click()
cy.url().should('include', '/dashboard')
cy.get('[data-test="welcome-message"]').should('contain', '欢迎回来')
})
it('密码错误时提示错误信息', () => {
cy.get('[data-test="username"]').type('admin')
cy.get('[data-test="password"]').type('wrong-password')
cy.get('[data-test="login-btn"]').click()
cy.get('[data-test="error-message"]').should('be.visible')
cy.get('[data-test="error-message"]').should('contain', '账号或密码错误')
})
})
这里有个非常重要的实操习惯:给交互元素加上data-test属性,而不是依赖class或id。理由是CSS类名可能因为样式调整频繁变动,一旦改了类名,E2E用例就挂了,但你改的只是样式,功能完全没变。用data-test是测试专用的“锚点”,开发改样式时不会误操这个属性,两边互不干扰。
Cypress的API风格是链式调用的,所有命令都返回一个链对象,命令之间是自动等待和重试的,不需要手动写sleep。Cypress会在断言失败前自动重试一段时间(defaultCommandTimeout默认4秒,可配置),这大大提高了E2E测试的稳定性。
4.4 E2E执行策略:本地调试还是流水线运行
E2E测试不能只在自己电脑上跑,不然就成了摆设。我见过很多项目“本地能过,一跑CI就挂”,原因基本是网络环境差异、接口依赖不稳定、或者测试数据和开发环境冲突。
要让E2E测试稳定落地,这三个原则必须坚持:
第一,E2E测试只跑关键路径。登录、主流程跳转、核心数据提交、权限拦截,这些每条都不能少;但像“设置中心里改头像”这种低频操作,加了也是拖累。
第二,测试环境数据可控。E2E测试跑起来会真实写入数据,所以必须有独立的测试数据库或内存mock。推荐的做法是为E2E专门配置一套mock接口服务,所有网络请求都从本地mock数据返回,彻底避免刷新后数据对不上的问题。
第三,执行时机上,开发阶段本地跑Cypress的watch模式调试,提交代码前跑一次cypress run单次执行,CI阶段再挂到流水线上。跑流水线时不需要打开可视化面板,直接用命令行模式输出结果即可。
bash复制npx cypress run --browser chrome --headless
headless模式的Cypress会在后台执行,生成测试报告和失败截图,CI里看到失败信息也能快速定位是哪个用例挂在哪一步。
5. 常见问题与排查技巧实录
5.1 单元测试报错速查表
这个表里的问题我在指导别人写Vue测试时遇到了无数次,整理出来供排查参考:
| 报错现象 | 常见原因 | 解决办法 |
|---|---|---|
Cannot use import statement outside a module |
Vitest配置里environment没设,或文件被node_modules外的路径引用 | 确认test.environment为node/jsdom,检查include路径 |
SyntaxError: Cannot use import statement outside a module |
组件里用了ESM但没有走Vite插件 | 确认plugins里配了@vitejs/plugin-vue |
window is not defined |
environment不是jsdom | test配置里加environment: 'jsdom' |
document is not defined |
同上 | 同上 |
Failed to resolve import "XXX" |
用了别名但Vitest没解析 | 在test配置里加resolve.alias,复用vite的别名配置 |
Cannot find module 'vue-router' |
组件用了路由,但测试没注入 | 在global.plugins里注入router实例 |
wrapper.find(...).trigger is not a function |
元素不存在,find返回了空wrapper | 先用exists()确认元素存在再trigger |
TestingLibraryElementError: Unable to find an element |
组件异步渲染还没完成就断言 | 加await,用find的异步版本如find()代替get |
timeout of 1000ms exceeded |
测试里有网络请求或真实定时器 | mock掉异步操作,或用fake timers |
nextTick is not a function |
Vue版本或导入方式不一致 | 从vue包里导入nextTick |
Cannot read properties of undefined (reading 'setValue') |
没有正确mount组件或元素不是input | 确认mount的组件包含input元素 |
| 中文文本断言失败 | text()返回包含多余空白字符 | 用trim()或contains匹配 |
最让我觉得“值得单独拎出来说”的是第五个报错——别名解析。Vue项目里几乎一定会配@指向src目录,但Vitest默认不会读vite.config.ts里的resolve.alias吗?其实会,但有个前提:你的test配置写在同一个vite.config.ts里。如果你为了整理代码把test配置拆到了单独的vitest.config.ts,那这个文件就得自己再写一遍resolve.alias了。很多项目迁移Vitest时踩的就是这个坑。
5.2 E2E测试运行不稳定的排查
Cypress用例偶尔挂一次,这种间歇性失败最让人头疼。按这个顺序排查:
先看失败截图。Cypress失败时会自动截屏,截图里如果能看到页面还停留在加载状态,大概率是元素等待时间不够。解决方式是改用更具体的断言,比如等一个网络请求完成而不是等一个按钮可见;或者在访问页面时用cy.intercept()拦截关键请求,然后用cy.wait()显式等待它返回。
再看网络请求。Cypress的DevTools面板里能看到所有请求,如果某个接口返回500或超时,那问题不在测试代码,而在后端服务。E2E测试比单元测试更依赖环境稳定性,所以建议E2E环境里的接口全部mock。
最后考虑资源竞争。开发服务器同时被多个测试占用时会出现端口冲突或资源占用过高,导致测试超时。CI环境的并发策略要设置合理,比如cypress run --parallel时需要额外配置负载均衡,新手阶段不建议开并行。
5.3 测试覆盖率怎么看,覆盖率多少才算“够”
Vitest的覆盖率报告可以直观看到哪些文件被测试到了、哪些分支没走到。运行npm run test:coverage后,terminal会输出一个表格,也可以生成html报告在浏览器里打开逐行查看。
覆盖率不是越高越好,追求100%会严重拖慢开发速度。我在实际项目里的经验线是:
- 工具函数和组合式函数:80%以上,这些纯逻辑代码测试成本低、收益高;
- 组件:60%到70%,重点覆盖有交互逻辑的组件,纯展示型组件可以放宽;
- store:覆盖所有actions和关键getters,states由actions间接覆盖;
- 路由守卫:每个守卫逻辑至少有一条用例,跳转、拦截、白名单三条必须保证。
记住一句话:覆盖率数据是给你找盲区用的,不是给你充KPI用的。看到哪个文件覆盖率低,先问自己“这个文件改动频繁吗”“它出问题影响大吗”,两个答案都是“是”,就值得补测试;否则可以缓一缓。
6. 测试代码里的一些习惯建议,踩过坑才懂
这部分说几个我在实际项目里因为测试踩过的坑,以及逐渐形成的个人习惯,供参考。
第一个习惯,尽量少用wrapper.vm直接访问组件内部方法。wrapper.vm在Vue Test Utils的vue2时代很常用,但在Vue 3里,它能访问到的API有限,而且直接调用组件内部方法等于在测试里建立了对组件实现的强耦合,一旦组件内部方法改名,测试就跟着挂。更稳的做法是模拟真实用户操作,用trigger触发事件,再通过DOM或emitted事件断言结果。
第二个习惯,测试文件命名要和被测试文件保持一致的路径结构。项目里我习惯把测试文件和源码放同一个目录,Counter.vue对应的测试就是Counter.spec.ts。这样找起来方便,也便于构建工具自动排除测试文件。如果你的团队约定把所有测试集中到tests目录,也没问题,但一定要在include配置里统一路径规则。
第三个习惯,afterEach里清理定时器、卸载组件、恢复环境。Vitest的fake timers如果不手动恢复,一个测试文件跑完会“感染”下一个测试文件,导致后者的定时器行为异常。所以用了vi.useFakeTimers()的地方,务必在afterEach里调用vi.useRealTimers()。
第四个习惯,不要把E2E测试和单元测试混着跑。两者环境不同:单元测试在jsdom里,E2E在真实的浏览器实例里。混着跑会让启动和执行时间暴增,也干扰彼此的资源。开发时单独跑,CI时分开两个stage。
7. 这个阶段结束后,测试还能怎么继续深入
到第57天把单元测试和E2E测试的基础流程跑通,已经算正式迈进了“测试思维”的大门。接下来还有几个方向值得继续探索:
组件测试可以继续深入快照测试(snapshot testing),它能在组件渲染结构发生变化时第一时间提醒你“这处改动是不是有意为之”。但快照测试的维护成本也高,组件频繁调整UI时可能产生大量无用快照变更。我的建议是:只在逻辑稳定的业务组件上使用快照,防的是有人无意中改了结构,不是防正常重构。
测试数据工厂(factory builder)是另一个实用性很高的方向。当测试用例多了之后,你会发现到处都在写“登录用户信息”这种重复的mock数据。抽成一个工厂函数,传入必要的差异字段,比如createUser({ role: 'admin' }),可以让每个测试用例的意图更清晰,也减少因为公共数据结构变化导致的大量测试代码修改。
变异测试(mutation testing)是更进阶的话题,它会自动修改你的业务代码然后看测试能否发现,用来评估测试用例的质量。这个工具目前在前端生态还比较小众,但思想值得了解:测试覆盖率50%不代表测得好,也许正好漏了核心逻辑,变异测试能帮你找出那些“假覆盖”。
E2E测试方面,可以研究Cypress的cy.session()来优化登录态复用,避免每条用例都跑一遍登录流程。把登录逻辑提取成自定义命令后,E2E用例的执行时间能缩短一半以上。
至于“Vue播放m3u8”“Vue DevTools插件”这类热点问题,跟测试不在一条线上,但学习路径中如果遇到,记住一个原则:不管加什么新功能、新依赖,都要重新审视它对现有测试的影响。比如引入新的第三方播放器组件后,之前mock的组件列表里可能就得补上它,否则测试环境会报“找不到组件”的错误。
学到第57天,能跑通单元测试和E2E测试,你的Vue项目就已经比绝大多数“能跑就行”的项目前进了一大截。以后接手的项目不管多乱,第一件事先补测试还是直接改代码,你心里会有一个比从前清晰得多的答案。测试不是负担,是你改代码时的安全网,这张网越早织,漏掉的bug就越少。
