前端的单元测试

问题场景:表单验证模块频繁出bug

系统里有个用户注册表单,包含手机号、密码强度、验证码等校验逻辑。由于多人协作,经常出现:

  • A改了手机号校验正则,B的密码强度检测突然失效
  • 修复一个边界case,引入另一个bug
  • 回归测试要手动点20次表单才能确认

最致命的是,这些逻辑都混在Vue组件里,根本没法单独验证。

解决方案:把校验逻辑拆出来单独测试

先把校验工具函数独立成validators.js

javascript 复制代码
// utils/validators.js
export const validatePhone = (phone) => {
  const regex = /^1[3-9]\d{9}$/; // 🔍 中国手机号正则
  return {
    isValid: regex.test(phone),
    error: phone ? '手机号格式不正确' : '手机号不能为空' // 🔍 空值特殊处理
  };
};

export const validatePassword = (password) => {
  const hasLength = password.length >= 8;
  const hasLetter = /[a-zA-Z]/.test(password);
  const hasNumber = /\d/.test(password);
  
  return {
    isValid: hasLength && hasLetter && hasNumber,
    score: [hasLength, hasLetter, hasNumber].filter(Boolean).length, // 🔍 强度评分
    error: !password ? '密码不能为空' : 
           !hasLength ? '密码至少8位' :
           !hasLetter ? '需包含字母' : 
           !hasNumber ? '需包含数字'
  };
};

现在可以写第一个测试了:

javascript 复制代码
// tests/validators.spec.js
import { validatePhone, validatePassword } from '../utils/validators';

describe('手机号校验', () => {
  test('正常手机号应通过', () => {
    const result = validatePhone('13812345678');
    expect(result.isValid).toBe(true); // ✅ 断言结果
  });

  test('空值应报错', () => {
    const result = validatePhone('');
    expect(result.isValid).toBe(false);
    expect(result.error).toBe('手机号不能为空');
  });

  test('错误格式应拦截', () => {
    const result = validatePhone('12345'); 
    expect(result.isValid).toBe(false);
    expect(result.error).toBe('手机号格式不正确');
  });
});

关键点在于:每个test只验证一个具体场景。不是在测试函数能不能运行,而是在验证业务规则是否被正确实现。

原理剖析:测试金字塔的底层逻辑

%% ========================================================= %% 专业级「测试金字塔」可视化 %% 主题:测试策略分层与占比 %% 适配:深色 / 浅色模式自适应 %% 图标:FontAwesome 6.5 %% ========================================================= %% 1. 全局样式 %% 2. 金字塔节点 %% 3. 占比标签 %% 4. 悬停提示 %% 5. 动效建议 %% ========== 1. 全局样式 ========== %% 主色:#10b981(emerald-500) %% 辅色:#0ea5e9(sky-500) %% 警告:#f59e0b(amber-500) %% 背景:#ffffff / #111827(自适应) %% 字体:Inter, system-ui, sans-serif %% 圆角:8px %% 阴影:0 4px 6px -1px rgba(0,0,0,0.1) graph TD classDef e2e fill:#ef4444,stroke:#dc2626,color:#fff,stroke-width:2px,rx:8px,ry:8px classDef integ fill:#f59e0b,stroke:#d97706,color:#fff,stroke-width:2px,rx:8px,ry:8px classDef unit fill:#10b981,stroke:#059669,color:#fff,stroke-width:2px,rx:8px,ry:8px classDef label fill:none,stroke:none,color:#374151,font-weight:600 %% ========== 2. 金字塔节点 ========== %% 使用梯形节点模拟金字塔 A["<i class='fa fa-robot'></i> E2E 测试"]:::e2e B["<i class='fa fa-puzzle-piece'></i> 集成测试"]:::integ C["<i class='fa fa-microscope'></i> 单元测试"]:::unit %% ========== 3. 占比标签 ========== A1["<div style='font-size:1.2em'>5%</div>"]:::label B1["<div style='font-size:1.2em'>20%</div>"]:::label C1["<div style='font-size:1.2em'>75%</div>"]:::label %% ========== 4. 悬停提示(title 属性) ========== click A "模拟真实用户操作:覆盖完整业务流程,成本高、速度慢" click B "组件间协作:验证模块接口与数据流,平衡速度与范围" click C "函数级验证:快速反馈、成本低,当前所处层级" %% ========== 5. 动效建议 ========== %% 部署时可通过 CSS 实现: %% .node[id*="C"] { animation: pulse 2s infinite; } %% 金字塔结构 A --> B B --> C A1 -.-> A B1 -.-> B C1 -.-> C

单元测试的核心是隔离 。看validatePhone的测试流程:

scss 复制代码
测试执行时序:
输入数据 → 调用被测函数 → 获取返回值 → 断言验证
   ↓           ↓               ↓            ↓
'138...' → validatePhone() → {isValid:true} → expect(...).toBe(true)

Jest会:

  1. 创建独立执行环境
  2. 注入测试数据
  3. 捕获函数返回值
  4. 用matcher进行断言
  5. 生成覆盖率报告

应用扩展:测试Vue组件的三种姿势

当逻辑进入组件层,情况变得复杂。比如这个密码输入框:

vue 复制代码
<!-- components/PasswordInput.vue -->
<template>
  <div class="password-field">
    <input 
      v-model="password" 
      type="password"
      @input="handleInput"
    />
    <div v-if="error" class="error">{{ error }}</div>
    <div class="strength-bar" :class="getStrengthClass()">
      强度: {{ strengthText }}
    </div>
  </div>
</template>

<script>
import { validatePassword } from '@/utils/validators';

export default {
  data() {
    return {
      password: '',
      error: ''
    };
  },
  computed: {
    strengthText() {
      const { score } = validatePassword(this.password);
      return ['极弱', '弱', '中等', '强'][score] || '';
    }
  },
  methods: {
    handleInput() {
      const { isValid, error } = validatePassword(this.password);
      this.error = isValid ? '' : error;
      this.$emit('update:valid', isValid); // 🔍 通知父组件
    },
    getStrengthClass() {
      return `level-${validatePassword(this.password).score}`;
    }
  }
};
</script>

测试策略要分层:

javascript 复制代码
// tests/PasswordInput.spec.js
import { mount } from '@vue/test-utils';
import PasswordInput from '@/components/PasswordInput.vue';

describe('PasswordInput组件', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = mount(PasswordInput);
  });

  test('输入弱密码显示错误提示', async () => {
    await wrapper.find('input').setValue('123');
    await wrapper.vm.$nextTick();
    
    expect(wrapper.find('.error').text())
      .toBe('密码至少8位'); // ✅ UI反馈验证
    
    expect(wrapper.emitted('update:valid')[0])
      .toEqual([false]); // ✅ 事件触发验证
  });

  test('密码强度条正确更新', async () => {
    await wrapper.find('input').setValue('abc12345');
    await wrapper.vm.$nextTick();
    
    expect(wrapper.find('.strength-bar').classes())
      .toContain('level-3'); // ✅ 样式类正确
  });
});

这里的关键是:

  • mount创建真实DOM环境
  • setValue模拟用户输入
  • emitted()捕获组件事件
  • $nextTick等待视图更新

主流方案对比:选型决策表

方案 优势 劣势 适用场景
Jest + Vue Test Utils 零配置、快照测试、覆盖率报告 需要学习wrapper API Vue项目标配
Vitest 速度极快、Vite原生支持 生态较新 新项目首选
Cypress Component 可视化调试、接近真实环境 启动慢、资源占用高 复杂交互组件
Jest DOM 简单断言、轻量 功能有限 纯工具函数

最终选择Vitest,因为:

javascript 复制代码
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './tests/setup.js', // 🔍 全局配置
    coverage: {
      provider: 'istanbul',
      reporter: ['text', 'html'] // 🔍 生成覆盖率报告
    }
  }
});

举一反三:三个变体场景

场景1:异步校验(验证码)

javascript 复制代码
// 需要模拟API调用
test('验证码错误显示提示', async () => {
  vi.mock('@/api/sms', () => ({
    checkCode: vi.fn().mockResolvedValue({ valid: false })
  }));
  
  await wrapper.find('#code-input').setValue('1234');
  expect(wrapper.text()).toContain('验证码错误');
});

场景2:组合式API

javascript 复制代码
// 测试useForm composable
test('useForm处理表单重置', () => {
  const { formData, reset } = useForm();
  formData.value.name = 'test';
  reset();
  expect(formData.value.name).toBe('');
});

场景3:第三方依赖

javascript 复制代码
// 模拟moment.js
vi.mock('moment', () => ({
  default: vi.fn(() => ({
    format: vi.fn(() => '2025-08-03')
  }))
}));

绿色的测试通过提示就像在说:"放心改,兄弟,我罩着你"。

相关推荐
mayaairi1 小时前
Vue2 事件绑定完全指南:从入门到原理
前端·javascript·vue.js
ℋᙚᵐⁱᒻᵉ鲸落7 小时前
移动端滑动手势冲突:overflow: auto导致外层横向滑动失效
前端·javascript·css·vue.js·html
默_笙8 小时前
🎃 前端学了 Next.js,后端该学啥?NestJS 就是 Node 版的蜜雪冰城
前端·javascript
前端snow9 小时前
ai agent --- 实现 openclaw 定时间效果
前端
码云之上10 小时前
换模型之后,Chatbot 为什么要自己做 compact?
前端·agent·前端工程化
星栈10 小时前
自定义 UI-UX-Pro-Max 的 CSV 知识库,把 AI 生成的后台拉回行业该有的样子
前端·agent·weui
研☆香10 小时前
简单的图片上传 删除 预览
前端
ITresearchGuest10 小时前
我用 AI 一小时写了一个世界杯数据可视化平台|前端 VibeCoding 初体验
前端·人工智能·信息可视化
慧一居士11 小时前
Vite项目中使用Less步骤,详细使用示例
前端·css
花间相见12 小时前
【LangChain组件03】—— LangChain Agents 执行与状态:工作流程与状态管理实战
java·前端·langchain